Profile anatomy and format
In this page we'll describe how to interpret a profile captured using the Self-Profiling API.
The format of the object returned byProfiler.stop() is designed to be space-efficient: for example, the format aims to avoid duplicating URL values for functions which are defined in the same script. This means that some interpretation is needed to understand how a sample in the profile object maps to a location in the program, and this guide page aims to explain how to perform this interpretation.
In the first section, we'll describe theabstract structure of a profile. In the next section we'll describethe format of the profile object returned byProfiler.stop(). Finally we'llwalk through an example to show what a profile for a given program looks like and how it can be interpreted.
In this article
Anatomy of a profile
In this section we'll describe the abstract structure of a profile. Note that this isn't the same as the format of the object returned by returned byProfiler.stop(): we'll describe that format in the next section of this guide.
A profile consists of an array of samples. Each sample consists of a timestamp and a call stack. Each call stack consists of an array of stack frames, and each stack frame contains information about the location of its corresponding function in the program:
The timestamp is aDOMHighResTimeStamp which measures milliseconds since thetime origin: for a window context, this is the time the window was created (if the window is new) or the time the browser started navigating to this document.
The call stack is a representation of the JavaScript call stack, which enables you to understand the execution path to the program's location at the point that the sample was taken.
The call stack consists of an array of stack frames. A stack frame essentially represents a nested function call, so if functiona() calls functionb(), which calls functionc(), and a sample is taken while the browser is executingc(), then the call stack will consist of frames[a, b, c]:
function c() { // sample taken here}function b() { c();}function a() { b();}Each stack frame contains information about the location of its corresponding function in the program:
- The URL of the script
- The name of the function
- The line number of the function definition in the script
- The column number of the function definition in the line
Profile format
Although the section above describes thelogical structure of a profile, the format of the object returned byProfiler.stop() is different. The reason is that the format is designed to be space-efficient: for example, the format aims to avoid duplicating URL values for functions which are defined in the same script.
The profile object contains four properties, all arrays:
framesAn array of objects, each containing information about a stack frame:
column: the column number of the function definition.line: the line number of the function definition.name: the name of the function.resourceId: the index of an item inresources, representing the URL of the script in which the function is defined.
Only
nameis always present: if the function is not defined in a script (for example, if it is a function that's built into the browser) then the other three properties are omitted.resourcesAn array of strings, each representing the URL of a script.
samplesAn array of object, each containing two properties:
timestamp: the time at which the sample was taken.stackId: the index of an element in thestacksarray.
stacksAn array of objects, each containing two properties:
frameId: the index of an element inframeswhich represents the most-nested frame in the stack.parentId: the index of another entry instacks, which represents the call stack up to but not including the frame represented byframeId. This is not present if the frame represented byframeIdwas at the top level of the stack.
Example
In the following example we have a web page that contains a button: when the user presses the button, the page generates some prime numbers.
The HTML contains only the button:
<button>generate!</button>The JavaScript is split across two files. The "main.js" script contains the click handler for the button. This starts a profile, then calls the code to generate the primes, then logs the resulting profile:
// main.jsimport { genPrimes } from "./generate.js";async function handleClick() { const profiler = new Profiler({ sampleInterval: 10, maxBufferSize: 10000 }); const primes = genPrimes(); console.log(`Finished generating ${primes.length} primes!`); const trace = await profiler.stop(); console.log(JSON.stringify(trace));}document.querySelector("#generate").addEventListener("click", handleClick);The "generate.js" script generates the primes, organized into two functions,genPrimes() andisPrime():
// generate.jsconst MAX_PRIME = 1000000000;const PRIMES_QUOTA = 10000;function isPrime(n) { for (let i = 2; i <= Math.sqrt(n); i++) { if (n % i === 0) { return false; } } return n > 1;}export function genPrimes() { const primes = []; while (primes.length < PRIMES_QUOTA) { const candidate = Math.floor(Math.random() * MAX_PRIME); if (isPrime(candidate)) { primes.push(candidate); } } return primes;}If we run this code, a profile such as the one below will be logged to the developer tools console:
{ "frames": [ { "name": "Profiler" }, { "column": 27, "line": 5, "name": "handleClick", "resourceId": 0 }, { "column": 17, "line": 6, "name": "isPrime", "resourceId": 1 }, { "column": 26, "line": 15, "name": "genPrimes", "resourceId": 1 } ], "resources": [ "http://localhost:3000/main.js", "http://localhost:3000/generate.js" ], "samples": [ { "stackId": 1, "timestamp": 2972.734999999404 }, { "stackId": 3, "timestamp": 2973.4899999946356 }, { "stackId": 3, "timestamp": 2974.5700000077486 }, { "stackId": 3, "timestamp": 2977.8649999946356 }, { "stackId": 3, "timestamp": 2978.4899999946356 }, { "stackId": 3, "timestamp": 2978.6950000077486 }, { "stackId": 3, "timestamp": 2978.9500000029802 }, { "stackId": 3, "timestamp": 2979.405000001192 }, { "stackId": 2, "timestamp": 2980.030000001192 }, { "stackId": 2, "timestamp": 2980.655000001192 } ], "stacks": [ { "frameId": 1 }, { "frameId": 0, "parentId": 0 }, { "frameId": 3, "parentId": 0 }, { "frameId": 2, "parentId": 2 } ]}This profile captured 10 samples, listed in thesamples property.
ThestackId property of each sample enables us to understand where the program was at the point the sample was taken, and in this case we've taken samples in three different places:
stackId: 1: one samplestackId: 3: seven samplesstackId: 2: two samples
To find the complete call stack for a sample, we retrieve the stack given thestackId, then use theframeId value in the stack to find the most-nested function, then recursively fetch parent stacks usingparentId, until we reach the top level, which doesn't have aparentId value.
For example, the diagram below shows how we could derive the complete call stack for the seven samples whosestackId is 3:
Note also that the first item inframes, which has aname value ofProfiler, represents a sample taken in theProfiler() constructor: since this is a function provided by the browser, the frame doesn't include script information.