Skip to content

Commit

Permalink
Snapshot
Browse files Browse the repository at this point in the history
  • Loading branch information
bdombro committed Mar 18, 2021
1 parent 8f63664 commit cd5b572
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 46 deletions.
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,9 @@ The location and capabilities of the server directly impact the audit results, a

Meanwhile, some websites are non-public and require custom engineering to allow the auditing computer to access.

### Decision #3: What to do with the metrics that Lighthouse produces?
### Decision #3: Does combining multiple audits together improve precision, and if so how many?

Lighthouse produces a lot of metrics, some of which are useful as-is, some are useful with some manipulation, some are not very useful.

Many metrics involve the addition of other metrics, which make it hard to compare a specific metric between audits. A good example is Largest Contentful Paint (LCP). LCP The time at which the image/text on the screen starts to settle. LCP is a valuable metric for software performance, but is useless when comparing one audit to another because of webserver influence. Mainly, LCP is highly influenced by the time it takes for the web-server to reply to page requests, also known as Time-to-First-Byte (SRT). The research demonstrated in ref #6 that metrics like LCP become useful when adjusted for TTFB.

Decision #4: Does combining multiple audits together improve precision, and if so how many?

Yes! Averaging is a great start, and can be made even better when accounting for botched audits. In ref #6, we utilize Standard Deviation to identify and reduce the impact of botched audits.
Yes! Averaging is a great start, and can be made even better when accounting for botched audits. In ref #6, we utilize a winsorizing algorithm, based on Standard Deviation, to identify and reduce the impact of outliers.

### Conclusion

Expand Down
86 changes: 48 additions & 38 deletions scripts/batch-lambda.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,79 @@ if [ -z "$OUT" ]; then
read OUT
fi

COUNT=$3
if [ -z "$COUNT" ]; then
echo -n "Count: "
read COUNT
fi

# If your target server is faulty, this can help ignore the failures
REDO_SERVER_FAILURES=$4
REDO_SERVER_FAILURES=$3
if [ -z "$REDO_SERVER_FAILURES" ]; then
echo -n "Re-try on empty website reply(y/n): "
read REDO_SERVER_FAILURES
REDO_SERVER_FAILURES=y
fi

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

rm -rf $OUT &> /dev/null
mkdir -p $OUT

SET_COUNT=5 # 5 seems like the magic number for not overwhelming the URL
NOW=`date '+%Y.%m.%d-%H:%M:%S'`

runner () {
local JOBNO=$1

local TRYCOUNT=$2
if [ -z "$TRYCOUNT" ]; then local TRYCOUNT=0; fi
let "TRYCOUNT=TRYCOUNT+1"
if [ "$TRYCOUNT" -gt 10 ]; then
printf "\nRetries failed for $JOBNO\n"
audit () {
local setNo=$1
local jobNo=$2
local tryNo=$3

if [ -z "$tryNo" ]; then local tryNo=0; fi
let "tryNo=tryNo+1"
if [ "$tryNo" -gt 10 ]; then
printf "\nRetries failed for $jobNo\n"
return
fi

local OUTFILE=$OUT/$NOW.$JOBNO.$TRYCOUNT.json
local TMPFILE=$OUT/$NOW.$JOBNO.$TRYCOUNT.tmp.json
local setPath=$OUT/$setNo
rm -rf $setPath &> /dev/null
mkdir -p $setPath
local outFile=$setPath/$NOW.$jobNo.$tryNo.json
local tmpFile=$outFile.tmp

aws lambda invoke --function-name lighthouse-benchmark-lambda --payload $PAYLOAD $TMPFILE > /dev/null
aws lambda invoke --function-name lighthouse-benchmark-lambda --payload $PAYLOAD $tmpFile > /dev/null

node $DIR/../aws/extractBodyFromResponse.js $TMPFILE > $OUTFILE
rm $TMPFILE
node $DIR/../aws/extractBodyFromResponse.js $tmpFile > $outFile
rm $tmpFile

# Sometimes lambda invoke fails
if [ ! -s $OUTFILE ]; then
printf "\nRe-trying $JOBNO due to lambda failure\n"
rm $OUTFILE &> /dev/null
runner $JOBNO $TRYCOUNT
if [ ! -s $outFile ]; then
printf "\nRe-trying $jobNo due to lambda failure\n"
rm $outFile &> /dev/null
audit $setNo $jobNo $tryNo
return
fi

# If your target server is faulty, this can help ignore the failures
if [ "$REDO_SERVER_FAILURES" == "y" ]; then
if [ "`jq '.runtimeError | .message' $OUTFILE | cat`" != "null" ]; then
printf "\nRe-trying $JOBNO due to server failure\n"
rm $OUTFILE
runner $JOBNO $TRYCOUNT
if [ "`jq '.runtimeError | .message' $outFile | cat`" != "null" ]; then
printf "\nRe-trying $jobNo due to server failure\n"
rm $outFile
audit $setNo $jobNo $tryNo
return
fi
fi
printf "."
}

createAuditSet () {
local setNo=$1
printf "¶ Set-$setNo: "
for jobNo in $( seq 1 $SET_COUNT ); do
audit $setNo $jobNo &
done
wait
printf "✅\n"
}

main () {
printf "¶ Warm-up: "
audit 0 0
printf "✅\n"
for i in $( seq 1 4 ); do
createAuditSet $i
done
printf "¶ DONE\n"
}

echo START
for i in $( seq 1 $COUNT ); do
runner $i &
done
wait
printf "\nDONE\n\n"
main
File renamed without changes.
176 changes: 176 additions & 0 deletions scripts/flatten-report-set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import * as fs from 'fs'
import * as path from 'path'
import '../src/lib/Math'


;(async function main(setDir: string) {

const lighthouseCols: Record<string, keyof typeof AuditName | 'time'> = {
'Time': 'time',
'Score': 'score',
'Time to First Byte (TTFB)': 'server-response-time',
'First Contentful Paint (FCP)': 'first-contentful-paint',
'Time to Interactive (TTI)': 'interactive',
'Total Blocking Time (TBT)': 'total-blocking-time',
'Largest Contentful Paint (LCP)': 'largest-contentful-paint',
'Cumulative Layout Shift (CLS)': 'cumulative-layout-shift',
'Total Byte Weight (TBW)': 'total-byte-weight',
}


if (!setDir) return console.log(`usage: node ${__filename} <setPath>`)
if (!fs.existsSync(setDir)) return console.error(setDir + " dir d.n.e.")

const allStats = getStats(setDir)
const statsFiltered = allStats.map(stat => pick(stat, Object.values(lighthouseCols)))
const mergedStats = mergeStats(statsFiltered)
const csv =
// Object.keys(lighthouseCols).join(',') + '\n' +
Object.values(mergedStats).join(',')

process.stdout.write(csv)
})(process.argv[2])

/**
* Transforms an array of audit stats into winsorized averages
* @param stats - array of audit stats
* @returns - record of stat name -> winsorized average
*/
function mergeStats(stats: Stat[]) {
const statNames = Object.keys(stats[0]) as (keyof Stat)[]
const merged = Object.fromEntries(statNames.map(statName => [statName, merge(stats.map(s => s[statName] as string))]))
return merged

// Transforms an array of numbers into a winsorized average
function merge(array: (number | string)[]) {
if (array.every(v => typeof v === 'number')) {
const bounded = boundToStdDeviation(array as number[])
const averages = Math.average(bounded)
// const averages = Math.average(array as number[])
const merged = averages.toFixed(2)
return merged
} else return array[0]
}

// A winsorizing algorthm based on Standard Deviation
function boundToStdDeviation(array: number[]) {
const avg = Math.average(array)
const stdDeviation = Math.standardDeviation(array)
const upperBound = avg + stdDeviation
const lowerBound = avg - stdDeviation
const bounded = array.map(v => Math.bound(v, lowerBound, upperBound))
return bounded
}
}



type Stat = Partial<Metrics> & {time: string}
function getStats(dir: string): Stat[] {
const stats: Stat[] = []
fs.readdirSync(dir).forEach(file => {
const report: Report = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf-8'))
const metrics = mapReportToMetrics(report)
stats.push({
time: report.fetchTime,
...metrics,
})
})
return stats

function mapReportToMetrics(report: Report) {
// benchmarkIndex environment
return Object.fromEntries(Object.entries(report.audits).map(([k,v]) => [k,v.numericValue])) as Metrics
}
}

export function pick<T extends Record<string, any>, K extends (keyof T)> (obj: T, keys: K[]): Pick<T, K> {
const res: Partial<Pick<T, K>> = {}
keys?.forEach(k => {
if (k in obj) res[k] = obj[k]
})
return res as Pick<T, K>
}



interface Report {
fetchTime: string
environment: {
benchmarkIndex: number
}
audits: Audits
}
type Audits = Record<keyof typeof AuditName, AuditMetric>
enum AuditName {
"score",
"first-contentful-paint",
"largest-contentful-paint",
"first-meaningful-paint",
"speed-index",
"screenshot-thumbnails",
"final-screenshot",
"estimated-input-latency",
"total-blocking-time",
"max-potential-fid",
"cumulative-layout-shift",
"server-response-time",
"first-cpu-idle",
"interactive",
"user-timings",
"critical-request-chains",
"redirects",
"mainthread-work-breakdown",
"bootup-time",
"uses-rel-preload",
"uses-rel-preconnect",
"font-display",
"diagnostics",
"network-requests",
"network-rtt",
"network-server-latency",
"main-thread-tasks",
"metrics",
"performance-budget",
"timing-budget",
"resource-summary",
"third-party-summary",
"third-party-facades",
"largest-contentful-paint-element",
"layout-shift-elements",
"long-tasks",
"non-composited-animations",
"unsized-images",
"preload-lcp-image",
"full-page-screenshot",
"uses-long-cache-ttl",
"total-byte-weight",
"offscreen-images",
"render-blocking-resources",
"unminified-css",
"unminified-javascript",
"unused-css-rules",
"unused-javascript",
"uses-webp-images",
"uses-optimized-images",
"uses-text-compression",
"uses-responsive-images",
"efficient-animated-content",
"duplicated-javascript",
"legacy-javascript",
"dom-size",
"no-document-write",
"uses-http2",
"uses-passive-event-listeners",
}
interface AuditMetric {
"id": string
"title": string
"description": string
"score": number
"scoreDisplayMode": string
"numericValue": number
"numericUnit": string
"displayValue": string
}
type Metrics = Record<keyof typeof AuditName, AuditMetric['numericValue']>
4 changes: 4 additions & 0 deletions scripts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["."]
}
59 changes: 59 additions & 0 deletions src/lib/Math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Extensions for Math
*/

interface Math {
average(arr: number[]): number
bound(unbound: number, lower: number, upper: number): number
standardDeviation(arr: number[], usePopulation?: boolean): number
}

Math.average = (arr) => {
return arr.reduce((a,v) => a+v, 0) / arr.length
}

/**
* Bound a number between two other numbers
* @param unbound - the unbound number input
* @param lower - the lower bound
* @param upper - the upper bound
* @returns
*/
Math.bound = (unbound, lower, upper) => {
return Math.max(Math.min(unbound, upper), lower)
}

/**
* Calc standard deviation
*
* When to use sample or population method
*
* We are normally interested in knowing the population standard deviation because
* our population contains all the values we are interested in. Therefore, you
* would normally calculate the population standard deviation if: (1) you have the
* entire population or (2) you have a sample of a larger population, but you are
* only interested in this sample and do not wish to generalize your findings to
* the population. However, in statistics, we are usually presented with a sample
* from which we wish to estimate (generalize to) a population, and the standard
* deviation is no exception to this. Therefore, if all you have is a sample, but
* you wish to make a statement about the population standard deviation from which
* the sample is drawn, you need to use the sample standard deviation. Confusion
* can often arise as to which standard deviation to use due to the name "sample"
* standard deviation incorrectly being interpreted as meaning the standard
* deviation of the sample itself and not the estimate of the population standard
* deviation based on the sample.
* Src: https://statistics.laerd.com/statistical-guides/measures-of-spread-standard-deviation.php
*
* @param arr
* @param usePopulation, use population method of calculating. The sample method is default.
* @returns standard deviation
*/
Math.standardDeviation = (arr, usePopulation) => {
const mean = arr.reduce((acc, val) => acc + val, 0) / arr.length;
return Math.sqrt(
arr
.reduce((acc, val) => acc.concat((val - mean) ** 2), [] as number[])
.reduce((acc, val) => acc + val, 0)
/ (arr.length - (usePopulation ? 0 : 1))
);
}

0 comments on commit cd5b572

Please sign in to comment.