|
| 1 | +// @flow |
| 2 | + |
| 3 | +/** |
| 4 | + * This implements a simple CLI to check existing public profiles for certain |
| 5 | + * criteria. |
| 6 | + * |
| 7 | + * To use it it first needs to be built: |
| 8 | + * yarn build-batch-checker |
| 9 | + * |
| 10 | + * Then it can be run from the `dist` directory: |
| 11 | + * node dist/batch-checker.js --hashes-file <path to text file> |
| 12 | + * |
| 13 | + * For example: |
| 14 | + * yarn build-batch-checker && node dist/batch-checker.js --hashes-file ~/Downloads/profile-hashes.txt |
| 15 | + * |
| 16 | + */ |
| 17 | + |
| 18 | +const fs = require('fs'); |
| 19 | + |
| 20 | +import { unserializeProfileOfArbitraryFormat } from '../profile-logic/process-profile'; |
| 21 | +import { getProfileUrlForHash } from '../actions/receive-profile'; |
| 22 | +import { getTimeRangeIncludingAllThreads } from '../profile-logic/profile-data'; |
| 23 | +import { encodeUintSetForUrlComponent } from '../utils/uintarray-encoding'; |
| 24 | + |
| 25 | +interface CliOptions { |
| 26 | + hashesFile: string; |
| 27 | +} |
| 28 | + |
| 29 | +function checkProfileThreadCPUDelta(profile: any, hash: string): Set<string> { |
| 30 | + const outcomes = new Set(); |
| 31 | + const rootRange = getTimeRangeIncludingAllThreads(profile); |
| 32 | + const { threads } = profile; |
| 33 | + for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) { |
| 34 | + const thread = threads[threadIndex]; |
| 35 | + const threadCPUDelta = thread.samples.threadCPUDelta; |
| 36 | + if (!threadCPUDelta) { |
| 37 | + outcomes.add('has thread without threadCPUDelta'); |
| 38 | + continue; |
| 39 | + } |
| 40 | + |
| 41 | + outcomes.add('has thread with threadCPUDelta'); |
| 42 | + const len = thread.samples.length; |
| 43 | + if (len < 2) { |
| 44 | + outcomes.add('has thread with fewer than two samples'); |
| 45 | + continue; |
| 46 | + } |
| 47 | + if (threadCPUDelta[0] === null) { |
| 48 | + outcomes.add('has null in first threadCPUDelta'); |
| 49 | + } |
| 50 | + const firstNonNullIndex = threadCPUDelta.findIndex((d) => d !== null); |
| 51 | + if (firstNonNullIndex !== -1) { |
| 52 | + for (let i = firstNonNullIndex + 1; i < len; i++) { |
| 53 | + if (threadCPUDelta[i] === null) { |
| 54 | + outcomes.add('has null after first null value in threadCPUDelta'); |
| 55 | + const sampleTime = thread.samples.time[i]; |
| 56 | + const relativeSampleTime = sampleTime - rootRange.start; |
| 57 | + const url = `https://profiler.firefox.com/public/${hash}/?v=10&thread=${encodeUintSetForUrlComponent(new Set([threadIndex]))}`; |
| 58 | + console.log( |
| 59 | + `non-null at sample ${i} on thread ${threadIndex} at relative time ${(relativeSampleTime / 1000).toFixed(3)}s: ${url}` |
| 60 | + ); |
| 61 | + break; |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + } |
| 66 | + return outcomes; |
| 67 | +} |
| 68 | + |
| 69 | +function checkProfileSchemaMatching(profile: any, _hash: string): Set<string> { |
| 70 | + const { meta, threads } = profile; |
| 71 | + const { markerSchema } = meta; |
| 72 | + const markerSchemaNames = new Set(markerSchema.map((schema) => schema.name)); |
| 73 | + const tracingCategories = new Set(); |
| 74 | + const textNames = new Set(); |
| 75 | + |
| 76 | + const outcomes = new Set(); |
| 77 | + for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) { |
| 78 | + const thread = threads[threadIndex]; |
| 79 | + const { markers, stringTable } = thread; |
| 80 | + for (let markerIndex = 0; markerIndex < markers.length; markerIndex++) { |
| 81 | + const nameIndex = markers.name[markerIndex]; |
| 82 | + const data = markers.data[markerIndex]; |
| 83 | + if ( |
| 84 | + data && |
| 85 | + data.type && |
| 86 | + data.type === 'tracing' && |
| 87 | + data.category && |
| 88 | + markerSchemaNames.has(data.category) |
| 89 | + ) { |
| 90 | + if (!tracingCategories.has(data.category)) { |
| 91 | + console.log( |
| 92 | + `Found tracing marker whose schema is for category ${data.category}, thread index ${threadIndex}, marker index ${markerIndex}` |
| 93 | + ); |
| 94 | + outcomes.add( |
| 95 | + `has tracing marker whose schema is for category ${data.category}` |
| 96 | + ); |
| 97 | + tracingCategories.add(data.category); |
| 98 | + } |
| 99 | + continue; |
| 100 | + } |
| 101 | + const name = stringTable.getString(nameIndex); |
| 102 | + if ( |
| 103 | + data && |
| 104 | + data.type && |
| 105 | + data.type === 'Text' && |
| 106 | + markerSchemaNames.has(name) |
| 107 | + ) { |
| 108 | + if (!textNames.has(name)) { |
| 109 | + console.log( |
| 110 | + `Found Text marker whose schema is for name ${name}, thread index ${threadIndex}, marker index ${markerIndex}` |
| 111 | + ); |
| 112 | + outcomes.add(`has Text marker whose schema is for name ${name}`); |
| 113 | + textNames.add(name); |
| 114 | + } |
| 115 | + continue; |
| 116 | + } |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + return outcomes; |
| 121 | +} |
| 122 | + |
| 123 | +function checkProfile(profile: Profile, hash: string): Set<string> { |
| 124 | + return checkProfileSchemaMatching(profile, hash); |
| 125 | +} |
| 126 | + |
| 127 | +export async function run(options: CliOptions) { |
| 128 | + const hashes = fs.readFileSync(options.hashesFile, 'utf8').split('\n'); |
| 129 | + console.log(`Have ${hashes.length} hashes.`); |
| 130 | + |
| 131 | + for (let i = 0; i < hashes.length; i++) { |
| 132 | + const hash = hashes[i]; |
| 133 | + console.log( |
| 134 | + `Checking profile ${i + 1} of ${hashes.length} with hash ${hash}` |
| 135 | + ); |
| 136 | + try { |
| 137 | + const response = await fetch(getProfileUrlForHash(hash)); |
| 138 | + const serializedProfile = await response.json(); |
| 139 | + const profile = |
| 140 | + await unserializeProfileOfArbitraryFormat(serializedProfile); |
| 141 | + if (profile === undefined) { |
| 142 | + throw new Error('Unable to parse the profile.'); |
| 143 | + } |
| 144 | + const outcome = checkProfile(profile, hash); |
| 145 | + console.log(`Outcome: ${[...outcome].join(', ')}`); |
| 146 | + } catch (e) { |
| 147 | + console.log(`Failed: ${e}`); |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + console.log('Finished.'); |
| 152 | +} |
| 153 | + |
| 154 | +export function makeOptionsFromArgv(processArgv: string[]): CliOptions { |
| 155 | + const argv = require('minimist')(processArgv.slice(2)); |
| 156 | + |
| 157 | + if (!('hashes-file' in argv && typeof argv['hashes-file'] === 'string')) { |
| 158 | + throw new Error( |
| 159 | + 'Argument --hashes-file must be supplied with the path to a text file of profile hashes' |
| 160 | + ); |
| 161 | + } |
| 162 | + |
| 163 | + return { |
| 164 | + hashesFile: argv['hashes-file'], |
| 165 | + }; |
| 166 | +} |
| 167 | + |
| 168 | +if (!module.parent) { |
| 169 | + try { |
| 170 | + const options = makeOptionsFromArgv(process.argv); |
| 171 | + run(options).catch((err) => { |
| 172 | + throw err; |
| 173 | + }); |
| 174 | + } catch (e) { |
| 175 | + console.error(e); |
| 176 | + process.exit(1); |
| 177 | + } |
| 178 | +} |
0 commit comments