@atlaskit/editor-plugin-interactivity
Version:
Interactivity plugin for @atlaskit/editor-core
148 lines (140 loc) • 5.87 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { bucketKeyForMs } from './bucket-boundaries';
/**
* Latencies are counted per 8 ms, the resolution Event Timing reports durations at. Every
* latency is rounded up to this step on the way in, which caps the number of distinct values
* a group can hold whatever the latency was derived from.
*/
const RESOLUTION_MS = 8;
/** Which percentiles are reported, as quantiles. */
const REPORTED_QUANTILES = [0.9, 0.98];
/**
* The latencies of one set of interactions — every interaction on the page, or only the ones
* inside the editor — reported as one object in the event.
*
* The whole state is a count per distinct latency, so an interaction only costs a counter
* whatever its latency was, and everything the event carries — the reported buckets, the
* count, the sum, the maximum and the percentiles — is derived from that map when a snapshot
* is taken. Nothing is computed while interactions arrive.
*
* The two counters count different populations: `trackInteractionUpdate` takes the interactions
* Event Timing measured, `countTotal` takes all of them, including the ones below the 16 ms
* reporting threshold it never delivers. So `totalCount >= observedCount`, and the difference is how
* many were too fast to be measured.
*/
export class InteractionGroup {
constructor() {
_defineProperty(this, "countByLatency", new Map());
_defineProperty(this, "totalCount", 0);
}
/**
* Takes in what the tracker now says about an interaction: a new one is counted, and one measured
* again moves the count it already has.
*
* @returns whether the group changed.
*/
trackInteractionUpdate(update) {
if (update.type === 'new') {
this.add(update.latencyMs);
return true;
}
return this.remeasure(update.previousLatencyMs, update.latencyMs);
}
add(latencyMs) {
this.increment(latencyMs);
}
/**
* Counts an interaction towards the group's total, measured or not. `page` has no use for it:
* `performance.interactionCount` counts the page's interactions.
*/
countTotal() {
this.totalCount += 1;
}
/**
* @returns whether the count moved, which is `false` when both latencies fall in the step the
* interaction is already counted in — including when the interaction was measured no slower at
* all and only its boundaries moved.
*/
remeasure(previousLatencyMs, latencyMs) {
if (this.roundLatencyUp(previousLatencyMs) === this.roundLatencyUp(latencyMs)) {
return false;
}
// Moved rather than counted again: the count belongs to the same interaction.
this.decrement(previousLatencyMs);
this.increment(latencyMs);
return true;
}
/**
* @param totalCount every interaction of the group, including those below the Event Timing
* reporting threshold. Defaults to what `countTotal` was told, which is where an editor
* group's total comes from; `page` passes `performance.interactionCount` instead.
*/
snapshot(totalCount = this.totalCount) {
var _latencies;
// Ascending, so the reported buckets come out in order and the last latency is the
// maximum. Sorted once for everything below.
const latencies = Array.from(this.countByLatency.keys()).sort((a, b) => a - b);
const observedCount = this.observedCount();
const percentileRanks = REPORTED_QUANTILES.map(quantile => ({
key: String(Math.round(quantile * 100)),
rank: Math.max(1, Math.ceil(quantile * observedCount))
}));
const buckets = {};
const percentilesMs = {};
let sumMs = 0;
let counted = 0;
for (const latencyMs of latencies) {
var _this$countByLatency$, _buckets$bucket;
const count = (_this$countByLatency$ = this.countByLatency.get(latencyMs)) !== null && _this$countByLatency$ !== void 0 ? _this$countByLatency$ : 0;
sumMs += latencyMs * count;
const bucket = String(bucketKeyForMs(latencyMs));
buckets[bucket] = ((_buckets$bucket = buckets[bucket]) !== null && _buckets$bucket !== void 0 ? _buckets$bucket : 0) + count;
// A percentile is the latency the group's interactions reach counting up from the
// fastest, so it is answered as soon as this many of them have been passed.
counted += count;
for (const {
key,
rank
} of percentileRanks) {
if (percentilesMs[key] === undefined && counted >= rank) {
percentilesMs[key] = latencyMs;
}
}
}
return {
// `performance.interactionCount` can lag the entries the observer has delivered.
totalCount: Math.max(totalCount, observedCount),
observedCount,
sumMs,
maxMs: (_latencies = latencies[latencies.length - 1]) !== null && _latencies !== void 0 ? _latencies : 0,
buckets,
percentilesMs
};
}
observedCount() {
let total = 0;
for (const count of this.countByLatency.values()) {
total += count;
}
return total;
}
/** Rounding lives here so that every count goes through the same step, in or out. */
roundLatencyUp(latencyMs) {
return Math.ceil(latencyMs / RESOLUTION_MS) * RESOLUTION_MS;
}
increment(latencyMs) {
var _this$countByLatency$2;
const step = this.roundLatencyUp(latencyMs);
this.countByLatency.set(step, ((_this$countByLatency$2 = this.countByLatency.get(step)) !== null && _this$countByLatency$2 !== void 0 ? _this$countByLatency$2 : 0) + 1);
}
decrement(latencyMs) {
var _this$countByLatency$3;
const step = this.roundLatencyUp(latencyMs);
const next = ((_this$countByLatency$3 = this.countByLatency.get(step)) !== null && _this$countByLatency$3 !== void 0 ? _this$countByLatency$3 : 0) - 1;
if (next > 0) {
this.countByLatency.set(step, next);
} else {
this.countByLatency.delete(step);
}
}
}