I am trying to reconstruct the exact sequence of executed Javascript functions (call graph) from log data gathered using the Tracing Profiler, particularly from the category “v8.cpu_profiler”.
Unfortunately, the number of nodes (function definitions) and edges (function calls) I obtain fluctuates across runs even if I interact with the test Web application in exactly the same way.
Currently, I use Puppeteer to extract the trace and interact with the application:
let browser = await puppeteer.launch({ headless: true, userDataDir: userDataFolder, args:[ `--disable-extensions`, `--js-flags=--jitless --no-opt --predictable --no-concurrent-recompilation `] // disable chrome's optimizations }); // Get the tab opened by default let [page] = await browser.pages(); // Make sure to disable the cache await page.setCacheEnabled(false); // create cdp session const cdp = await page.target().createCDPSession(); await cdp.send('Tracing.start', { traceConfig: { recordMode: 'recordContinuously', includedCategories: ['disabled-by-default-v8.cpu_profiler'], excludedCategories: ['*'] }, transferMode: 'ReturnAsStream' }); await page.waitForTimeout(1000); // Go to the page await Promise.race([ page.goto(this.url, { waitUntil: ["load", "networkidle2"] }), page.waitForTimeout("body"), ]); // Wait to make sure that the page is fully loaded await page.waitForTimeout(1000); // dynamically interact with the webpage await interact(page);
As you can see, I use the flags --jitless --no-opt --predictable --no-concurrent-recompilation
to disable any source of non-determinism I know of, but it is still not enough. Why does this happen? Theoretically, the executed functions are exactly the same and the Tracing Profiler should be very precise.
Is it possible that v8 is still doing some optimizations under the hood and if yes, how can I disable them?
Is there any other way I can extract the exact sequence of executed functions?
Advertisement
Answer
V8 has a --trace
flag, which gives an exact sequence of executed functions. Warning: it produces lots of output, because there are lots of function calls on typical websites.
The function call sequence is entirely deterministic. Internal optimizations have nothing to do with that, so there’s no reason to turn them off. (Think about it: if optimization changed the order in which functions called each other, that would break applications. Whatever an engine does internally must not change the behavior that the developer of the website intended.)
The “Tracing Profiler” is a sample-based profiler, and as such non-deterministic by design (and there can’t possibly be a flag to “turn that off”). It uses a ticker thread that samples the main thread every millisecond (or similar interval, I’m not sure about the exact value). What exactly the main thread is doing at this point will be different between different runs; it’s only statistically meaningful (e.g. if a function takes X% of the overall time, it’ll be represented on approximately X% of the sampling ticks).
Of course, JavaScript applications can have their own sources of non-determinism:
if (Math.random() < 0.5) { foo(); } else { bar(); }
will create different call sequences on different runs. It’s also conceivable that JavaScript reacts to mouse movements or other user interactions, which would be exceedingly difficult to replicate exactly.