@quick-game/cli
Version:
Command line interface for rapid qg development
201 lines • 8.48 kB
JavaScript
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';
// This handler serves two purposes. It generates a list of events that are
// used to show user clicks in the timeline. It is also used to gather
// EventTimings into Interactions, which we use to show interactions and
// highlight long interactions to the user, along with INP.
// We don't need to know which process / thread these events occurred in,
// because they are effectively global, so we just track all that we find.
const allEvents = [];
export const LONG_INTERACTION_THRESHOLD = Helpers.Timing.millisecondsToMicroseconds(Types.Timing.MilliSeconds(200));
let longestInteractionEvent = null;
const interactionEvents = [];
const interactionEventsWithNoNesting = [];
const eventTimingEndEventsById = new Map();
const eventTimingStartEventsForInteractions = [];
let handlerState = 1 /* HandlerState.UNINITIALIZED */;
export function reset() {
allEvents.length = 0;
interactionEvents.length = 0;
eventTimingStartEventsForInteractions.length = 0;
eventTimingEndEventsById.clear();
interactionEventsWithNoNesting.length = 0;
longestInteractionEvent = null;
handlerState = 2 /* HandlerState.INITIALIZED */;
}
export function handleEvent(event) {
if (handlerState !== 2 /* HandlerState.INITIALIZED */) {
throw new Error('Handler is not initialized');
}
if (!Types.TraceEvents.isTraceEventEventTiming(event)) {
return;
}
if (Types.TraceEvents.isTraceEventEventTimingEnd(event)) {
// Store the end event; for each start event that is an interaction, we need the matching end event to calculate the duration correctly.
eventTimingEndEventsById.set(event.id, event);
}
allEvents.push(event);
// From this point on we want to find events that represent interactions.
// These events are always start events - those are the ones that contain all
// the metadata about the interaction.
if (!event.args.data || !Types.TraceEvents.isTraceEventEventTimingStart(event)) {
return;
}
const { duration, interactionId } = event.args.data;
// We exclude events for the sake of interactions if:
// 1. They have no duration.
// 2. They have no interactionId
// 3. They have an interactionId of 0: this indicates that it's not an
// interaction that we care about because it hasn't had its own interactionId
// set (0 is the default on the backend).
// See: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/responsiveness_metrics.cc;l=133;drc=40c209a9c365ebb9f16fb99dfe78c7fe768b9594
if (duration < 1 || interactionId === undefined || interactionId === 0) {
return;
}
// Store the start event. In the finalize() function we will pair this with
// its end event and create the synthetic interaction event.
eventTimingStartEventsForInteractions.push(event);
}
/**
* See https://web.dev/better-responsiveness-metric/#interaction-types for the
* table that defines these sets.
**/
const pointerEventTypes = new Set([
'pointerdown',
'touchstart',
'pointerup',
'touchend',
'mousedown',
'mouseup',
'click',
]);
const keyboardEventTypes = new Set([
'keydown',
'keypress',
'keyup',
]);
export function categoryOfInteraction(interaction) {
if (pointerEventTypes.has(interaction.type)) {
return 'POINTER';
}
if (keyboardEventTypes.has(interaction.type)) {
return 'KEYBOARD';
}
return 'OTHER';
}
/**
* We define a set of interactions as nested where:
* 1. Their end times align.
* 2. The longest interaction's start time is earlier than all other
* interactions with the same end time.
* 3. The interactions are of the same category [each interaction is either
* categorised as keyboard, or pointer.]
*
* =============A=[pointerup]=
* ====B=[pointerdown]=
* ===C=[pointerdown]==
* ===D=[pointerup]===
*
* In this example, B, C and D are all nested and therefore should not be
* returned from this function.
*
* However, in this example we would only consider B nested (under A) and D
* nested (under C). A and C both stay because they are of different types.
* ========A=[keydown]====
* =======B=[keyup]=====
* ====C=[pointerdown]=
* =D=[pointerup]=
**/
export function removeNestedInteractions(interactions) {
/**
* Because we nest events only that are in the same category, we store the
* longest event for a given end time by category.
**/
const earliestEventForEndTimePerCategory = {
POINTER: new Map(),
KEYBOARD: new Map(),
OTHER: new Map(),
};
function storeEventIfEarliestForCategoryAndEndTime(interaction) {
const category = categoryOfInteraction(interaction);
const mapToUse = earliestEventForEndTimePerCategory[category];
const endTime = Types.Timing.MicroSeconds(interaction.ts + interaction.dur);
const earliestCurrentEvent = mapToUse.get(endTime);
if (!earliestCurrentEvent) {
mapToUse.set(endTime, interaction);
return;
}
if (interaction.ts < earliestCurrentEvent.ts) {
mapToUse.set(endTime, interaction);
}
}
for (const interaction of interactions) {
storeEventIfEarliestForCategoryAndEndTime(interaction);
}
// Combine all the events that we have kept from all the per-category event
// maps back into an array and sort them by timestamp.
const keptEvents = Object.values(earliestEventForEndTimePerCategory)
.flatMap(eventsByEndTime => Array.from(eventsByEndTime.values()));
keptEvents.sort((eventA, eventB) => {
return eventA.ts - eventB.ts;
});
return keptEvents;
}
export async function finalize() {
// For each interaction start event, find the async end event by the ID, and then create the Synthetic Interaction event.
for (const interactionStartEvent of eventTimingStartEventsForInteractions) {
const endEvent = eventTimingEndEventsById.get(interactionStartEvent.id);
if (!endEvent) {
// If we cannot find an end event, bail and drop this event.
continue;
}
if (!interactionStartEvent.args.data?.type || !interactionStartEvent.args.data?.interactionId) {
// A valid interaction event that we care about has to have a type (e.g.
// pointerdown, keyup).
//
// We also need to ensure it has an interactionId. We already checked
// this in the handleEvent() function, but we do it here also to satisfy
// TypeScript.
continue;
}
const interactionEvent = {
// Use the start event to define the common fields.
cat: interactionStartEvent.cat,
name: interactionStartEvent.name,
pid: interactionStartEvent.pid,
tid: interactionStartEvent.tid,
ph: interactionStartEvent.ph,
args: {
data: {
beginEvent: interactionStartEvent,
endEvent: endEvent,
},
},
ts: interactionStartEvent.ts,
dur: Types.Timing.MicroSeconds(endEvent.ts - interactionStartEvent.ts),
type: interactionStartEvent.args.data.type,
interactionId: interactionStartEvent.args.data.interactionId,
};
if (!longestInteractionEvent || longestInteractionEvent.dur < interactionEvent.dur) {
longestInteractionEvent = interactionEvent;
}
interactionEvents.push(interactionEvent);
}
handlerState = 3 /* HandlerState.FINALIZED */;
interactionEventsWithNoNesting.push(...removeNestedInteractions(interactionEvents));
}
export function data() {
return {
allEvents: [...allEvents],
interactionEvents: [...interactionEvents],
interactionEventsWithNoNesting: [...interactionEventsWithNoNesting],
longestInteractionEvent,
interactionsOverThreshold: new Set(interactionEvents.filter(event => {
return event.dur > LONG_INTERACTION_THRESHOLD;
})),
};
}
//# sourceMappingURL=UserInteractionsHandler.js.map