resource-timing icon indicating copy to clipboard operation
resource-timing copied to clipboard

Expose JavaScript code caching information in PerformanceResourceTiming

Open mathiasbynens opened this issue 5 years ago • 7 comments

Several modern JavaScript engines (V8, SpiderMonkey, and soon JavaScriptCore) implement code caching, a feature that caches the result of parsing & compiling bytecode for scripts. Code-cached scripts can start to execute much sooner.

Currently, the Resource Timing API doesn't provide info on whether a given script was code-cached or not, even though this significantly impacts its metrics. I'd like to propose exposing code caching information for scripts in the Resource Timing API.


One way this could be done is to allow UAs to add a new boolean isCodeCached property to the PerformanceResourceTiming interface. Illustrative example that gets all the code-cached script entries:

performance.getEntries()
  .filter(item => item.initiatorType === 'script') // This part already works today.
  .map(item => item.isCodeCached) // This would be the new property.

Developers could then check for the presence of this property, and report it alongside the other metrics when available.

What do people think?

mathiasbynens avatar Jan 02 '19 15:01 mathiasbynens

This would be really useful for us in Gmail. I believe code caching has a lot of opportunity for us to improve page load times since we depend on a large chunk of JS to draw the page, but without more detailed data it's really hard to know how much potential is there. Here's how I think we could use this:

  1. If the data is available via resource timing, we'll read the cache state for our main JS blob and annotate it onto a custom timer for our page load latency. We're less interested (for now) in code caching of secondary resources.

  2. From this data we'll compute the overall hit rate and compare aggregate page load time with a breakdown indicating whether the main JS resource was a code cache hit or miss.

  3. With that information we can estimate the impact of improving our code caching hit rate on page load time, then implement some conditional logic we expect to improve code caching: fetch new app shell versions less aggressively or register a temporary service worker that will cache the code in its install event, thereby triggering code caching.

  4. Enable the conditional logic for some small test and control groups to try it out, and then compare page load times and code caching hit rates between them.

  5. If code caching hit rate improves and load times meaningfully decrease, we can decide to move forward with a full-fledged feature. If code caching hit rate improves but effect on load times is negligible we'll shelve this idea for later. If code caching hit rate does not improve we'll have to do more investigation to find out why.

Hope that helps to make the case for a feature like this. Thanks!

nmckay avatar Jan 07 '19 16:01 nmckay

The largest risk in my opinion is privacy. If privacy risks can be overcome, this seems promising as a way to inform developers of benefits of using the same cached JS. I'm not a privacy expert so I'd want a privacy expert to review this to understand whether there is a risk.

I like this idea!

In terms of whether the flag is enough, different implementations have different heuristics and efficiency.

I'm personally in favor of adding the parse time, load time to the resource to allow this to be reasoned about separate from networking time. How to report that time is interesting: it breaks into 3 pieces: time in queue or blocked by other work, background work and UI thread work. I'm not certain of the best way to communicate these times but all are valuable to a web developer in performing analysis. Totally open to ideas on exposing more information here!

toddreifsteck avatar Jan 10 '19 19:01 toddreifsteck

That'd be great @mathiasbynens! However, I was thinking if such information would be better surfaced with the consuming timing info (i.e. the time taken to fetch from disk an already code cached script) instead of a boolean flag. This would be similar to what we currently have in transferSize which shows resource was actually fetched from network, not from cache. Consuming a cached code should never be 0 (assuming it's using monotonic DOMHighRes). Besides the time taken to fetch the cached code, it'd be nice to have the final bytecode size. This would allow tracking the ratio of served script size by its final bytecode (cached) size, as well as estimating the amount of code actually cached (assuming those cases when not the whole script bytecode is cached, only the top-level execution functions). Not sure how other JS engines produce the cached code. Assuming it's similar to V8, such information could also be provided in lieu of consumed cached code. E.g.: First time (or the one after that (warm run)) visiting site:

performance.getEntriesByName('main.js')[0];
{
  // ...
  codeCacheType: 'produced',
  codeCacheDuration: 400.00000,
  codeCacheSize: 1048576
}

Next visits:

performance.getEntriesByName('main.js')[0];
{
  // ...
  codeCacheType: 'consumed',
  codeCacheDuration: 50.00000,
  codeCacheSize: 1048576
}

I used codeCacheDuration above instead of codeCacheStart and codeCacheEnd as I'm not sure if those particular start and end timing would be useful. Notice that codeCacheDuration must be ≤ duration from PerformanceEntry.

Thoughts?

@toddreifsteck regarding privacy, I think code cache information could follow the existing same origin policy already enforced by Resource Timing API via Timing-Allow-Origin response header. Therefore, only white-listed origins could get such code cache information avoiding an attacker to infer if a given script is actually on the user's cache or how long it took to produce/consume the script cached code (possibly infering user's CPU processing power).

marcelduran avatar Jan 11 '19 08:01 marcelduran

I don't think the proposed API makes much sense for WebKit/JSC since JSC's byte code cache is per JS function.

rniwa avatar Jan 16 '19 02:01 rniwa

We had a short discussion about this and I wanted to leave a quick note about the security of this API.

In general, there seem to be ways of implementing this without running into cross-origin leaks, e.g. what @marcelduran mentioned above about keying the cache so that it's impossible for a cross-origin document to infer if a given script was already cached by a victim origin. However, implementers would have to be careful to design their cache in a way that doesn't expose them to this class of attacks.

For example, let's assume a naive cache where the key are the contents of the script (or their hash), and the value is the bytecode. In this case, an attacker would get an oracle for the contents of arbitrary cross-origin scripts -- she could seed the cache with a set of attacker-controlled scripts, load https://victim.example/foo.js and inspect its isCodeCached property to see if the script was identical to one of the attacker's prior guesses, leaking information.

I believe that Chrome already prevents this by double-keying the cache on the loading origin + the URL of the script, in which case the attack doesn't work. The main thing would be to capture this in the spec to make sure other implementations get similar protections.

arturjanc avatar Feb 08 '19 15:02 arturjanc

Checking back in here. Since browsers implement bytecode caching differently (e.g. in JSC it's on a per-function level, whereas in V8 it's per script), it doesn't seem feasible to standardize anything that exposes "bytecode cache state" at the script level.

Instead, we could add more metrics to script entries, possibly parse{Start,End}, compile{Start,End}, execute{Start,End} which would expose more generic information. (Note that one could then still figure out whether or not a given script was code-cached in V8, in user-land code: the script entry would have near-zero parse + compile times.)

mathiasbynens avatar Jan 28 '20 08:01 mathiasbynens

Note that the implementation in JSC just got changed so that it's per file. However, the fact we used to have a per-function cache is a good indication that we don't want to make assumptions about how these caches would work (e.g. we may change our cache back to per-function in the future again).

rniwa avatar Jan 29 '20 07:01 rniwa