UNPKG

@atlaskit/editor-plugin-interactivity

Version:

Interactivity plugin for @atlaskit/editor-core

148 lines (140 loc) 5.87 kB
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); } } }