Skip to content

Instantly share code, notes, and snippets.

@Domiii
Created February 27, 2024 12:03
Show Gist options
  • Save Domiii/8d6372af8bb267a9db96ec71a2d9d5ee to your computer and use it in GitHub Desktop.
Save Domiii/8d6372af8bb267a9db96ec71a2d9d5ee to your computer and use it in GitHub Desktop.
Replay Devtools Breakpoint Visualizer + more snippets
/* global app, BigInt */
// chrome-extension://dhdgffkkebhmkfjojejmpbldmpobfkfo/options.html#nav=5c343cce-0abe-4d62-8634-6e8aec7157ac+editor
/** ###########################################################################
* query frontend app
* ##########################################################################*/
const getPauseId = window.getPauseId = () => {
const state = app.store.getState();
const pauseId = state?.pause?.id;
if (!pauseId) {
throw new Error(`Pause required (but not found) for snippet`);
}
return pauseId;
};
const getSessionId = window.getSessionId = () => {
const state = app.store.getState();
const sessionId = state?.app?.sessionId;
if (!sessionId) {
throw new Error(`sessionId required (but not found) for snippet`);
}
return sessionId;
};
// (() => {
// const state = app.store.getState();
// const sessionId = state?.app?.sessionId;
// if (!sessionId) {
// throw new Error(`sessionId required (but not found) for snippet`);
// }
// return sessionId;
// })()
const getAllFramesForPause = window.getAllFramesForPause = async (pauseId) => {
return await app.client.Pause.getAllFrames(
{},
window.getSessionId(),
pauseId || window.getPauseId()
);
};
/** ###########################################################################
* point stuff
* ##########################################################################*/
const FirstCheckpointId = 1;
/**
* Copied from backend/src/shared/point.ts
*/
function pointToBigInt(point) {
let rv = BigInt(0);
let shift = 0;
if (point.position) {
addValue(point.position.offset || 0, 32);
switch (point.position.kind) {
case "EnterFrame":
addValue(0, 3);
break;
case "OnStep":
addValue(1, 3);
break;
// NOTE: In the past, "2" here indicated an "OnThrow" step type.
case "OnPop":
addValue(3, 3);
break;
case "OnUnwind":
addValue(4, 3);
break;
default:
throw new Error("UnexpectedPointPositionKind " + point.position.kind);
}
// Deeper frames predate shallower frames with the same progress counter.
console.assert(
point.position.frameIndex !== undefined,
"Point should have a frameIndex",
{
point,
}
);
addValue((1 << 24) - 1 - point.position.frameIndex, 24);
// Points with positions are later than points with no position.
addValue(1, 1);
} else {
addValue(point.bookmark || 0, 32);
addValue(0, 3 + 24 + 1);
}
addValue(point.progress, 48);
// Subtract here so that the first point in the recording is 0 as reflected
// in the protocol definition.
addValue(point.checkpoint - FirstCheckpointId, 32);
return rv;
function addValue(v, nbits) {
rv |= BigInt(v) << BigInt(shift);
shift += nbits;
}
}
/** ###########################################################################
* Code serialization utilities
* ##########################################################################*/
function serializeFunctionCall(f) {
var code = `(eval(eval(${JSON.stringify(f.toString())})))`;
code = `(${code})()`;
return JSON.stringify(`dev:${code}`);
}
function testRunSerializedExpressionLocal(expression) {
// NOTE: Extra parentheses are added in frontend sometimes
expression = `(${expression})`;
var cmd = expression;
if (cmd.startsWith('(')) {
// strip "()"
cmd = cmd.substring(1, expression.length - 1);
}
// parse JSON (used for serialization)
cmd = JSON.parse(cmd);
// strip "dev:" and run
cmd = `(${cmd.substring(4)})`;
eval(cmd);
}
/** ###########################################################################
* {@link chromiumEval} executes arbitrary code inside `chromium`
* ##########################################################################*/
window.chromiumEval = async (expression) => {
if (expression instanceof Function) {
// serialize function
expression = serializeFunctionCall(expression);
}
const x = await app.client.Pause.evaluateInGlobal(
{
expression,
pure: false,
},
getSessionId(),
getPauseId()
);
try {
}
catch (err) {
console.error(`unable to parse returned value:`, x, '\n\n');
throw err;
}
const {
result: {
data,
returned: {
value
} = {},
exception: {
value: errValue
} = {}
}
} = x;
if (errValue) {
throw new Error(errValue);
}
return value;
};
/** ###########################################################################
* util
* ##########################################################################*/
window.flushCommandErrors = async () => {
let err
// NOTE: can cause infinite loop if `chromiumEval` itself induces errors
while ((err = await chromiumEval(() => DevOnly.popCommandError()))) {
console.error(err);
}
};
/** ###########################################################################
* DOM protocol queries
* ##########################################################################*/
async function getAllBoundingClientRects() {
try {
const { elements } = await app.client.DOM.getAllBoundingClientRects(
{},
getSessionId(),
getPauseId()
);
return elements;
}
finally {
await flushCommandErrors();
}
};
async function getBoxModel(node) {
try {
const result = await app.client.DOM.getBoxModel(
{ node },
getSessionId(),
getPauseId()
);
return result;
}
finally {
await flushCommandErrors();
}
}
async function DOM_getDocument() {
try {
const result = await app.client.DOM.getDocument(
{},
getSessionId(),
getPauseId()
);
return result;
}
finally {
await flushCommandErrors();
}
}
/** ###########################################################################
* High-level tools.
* ##########################################################################*/
let lastCreatedPause;
function getCreatedPause() {
return lastCreatedPause;
}
async function pauseAt(pointStruct) {
const point = pointToBigInt(pointStruct).toString();
lastCreatedPause = await app.client.Session.createPause({ point }, getSessionId());
console.log(`Paused at ${lastCreatedPause?.pauseId}:`, lastCreatedPause);
}
async function getAllFrames() {
const pause = getCreatedPause();
if (!pause?.pauseId) {
throw new Error(`Not paused at a good point.`);
}
if (pause?.pauseId) {
const res = await getAllFramesForPause(pause.pauseId);
console.log(`getAllFrames:`, res);
}
}
async function getTopFrame() {
const { data: { frames } } = await app.client.Pause.getAllFrames({}, sessionId, getPauseId());
const topFrame = frames[0];
return topFrame;
}
function getSelectedLocation() {
const loc = app.store.getState().sources?.selectedLocation;
if (!loc) {
throw new Error(`No source selected`);
}
return loc;
}
function getSelectedSourceId() {
return getSelectedLocation().sourceId;
}
async function selectLocation(sourceId, loc = undefined) {
return app.actions.selectLocation(loc, {
sourceId: sourceId + ""
});
}
function getSourceText(line) {
return document.querySelector(`[data-test-id="SourceLine-${line}"] [data-test-formatted-source="true"]`).textContent;
}
// getSourceText(24713);
/** ###########################################################################
* DOM Manipulation
* ##########################################################################*/
function getElTestString(el, name) {
return el.getAttribute(`${name}`);
}
function getElTestNumber(el, name) {
return parseInt(el.getAttribute(`${name}`));
}
function isElVisible(e) {
return !!( e.offsetWidth || e.offsetHeight || e.getClientRects().length );
}
function getVisibleLineEls() {
const lineEls = Array.from(document.querySelectorAll("[data-test-line-number]")).filter(isElVisible);
const lineNums = lineEls.map(el => getElTestNumber(el, "data-test-line-number"));
return {
lineEls,
lineNums
};
}
/**
* @example getSourceLineChildElements(24713).columnEls[1]
*/
function getSourceLineChildElements(line) {
const lineEl = document.querySelector(`[data-test-line-number="${line}"`);
if (!lineEl) {
return null;
}
const columnEls = Array.from(lineEl.querySelectorAll(`[data-column-index]`))
if (!columnEls.length) {
return null;
}
const columnIndexes = columnEls.map(el => getElTestNumber(el, "data-column-index"));
return {
columnEls,
columnIndexes
}
}
function insertIntoString(str, idx, toInsert) {
return str.slice(0, idx) + toInsert + str.slice(idx);
}
function reset() {
removeCustomEls();
}
function removeCustomEls() {
const customEls = Array.from(document.querySelectorAll("[data-custom]"));
for (const el of customEls) {
el.remove();
}
}
async function insertSourceBreakpoints() {
removeCustomEls();
const loc = app.store.getState().sources?.selectedLocation;
if (!loc) {
throw new Error(`insertSourceBreakpoints requires selected location`);
}
const { lineLocations } = await app.client.Debugger.getPossibleBreakpoints(
{ sourceId: loc.sourceId },
sessionId
);
// const sourceIdNum = loc.sourceId.match(/\d+/)[0];
// const sources = app.store.getState().sources.sourceDetails.entities[sourceIdNum];
// console.log(sources);
// const lineMin = nLineFrom;
// const lineMax = lineMin + nLineDelta;
// const locs = lineLocations.filter(l => l.line >= lineMin && l.line <= lineMax);
const breakpointLocsByLine = Object.fromEntries(lineLocations.map(loc => [loc.line, loc]));
// const breakpointLocations = locs.flatMap(l => {
// return l.columns?.map(c => `${l.line}:${c}`) || "";
// });
const { lineEls, lineNums } = getVisibleLineEls();
for (let j = 0; j < lineEls.length; ++j) {
const line = lineNums[j];
const breaks = breakpointLocsByLine[line];
if (breaks) {
// Modify line
const sourceEls = getSourceLineChildElements(line);
if (!sourceEls) {
continue;
}
// if (line === 24713)
// debugger;
const { columnEls, columnIndexes } = sourceEls;
for (let col of breaks.columns) {
// Iterate column back to front, so modification does not mess with follow-up indexes.
const iCol = columnIndexes.findLastIndex((idx, i) => idx < col && (i === columnIndexes.length || columnIndexes[i+1] >= col));
const targetEl = columnEls[iCol];
const iOffset = col - columnIndexes[iCol];
if (!targetEl) {
debugger;
continue;
}
targetEl.innerHTML = insertIntoString(targetEl.innerHTML, iOffset, `<span data-custom="1" style="color: red">|</span>`);
}
}
}
// locs.forEach(l => {
// const source = getSourceText(l.line);
// const highlightCss = "color: red";
// const clearCss = "color: default";
// let lastIdx = 0;
// const sourceParts = l.columns.map((col, i) => {
// return source.slice(lastIdx, lastIdx = l.columns[i]);
// });
// sourceParts.push(source.slice(lastIdx));
// const format = sourceParts.join("<BREAK>");
// console.log(format);
// });
// console.log(...printArgs);
// return {
// breakpointLocations
// };
}
async function getHitCounts() {
const loc = getSelectedLocation();
console.log("loc", loc);
const minCol = 0;
const maxCol = 100;
const { lineLocations } = await app.client.Debugger.getHitCounts(
{
sourceId: loc.sourceId,
locations: [{
line: loc.line,
columns: range(minCol, maxCol)
}]
},
sessionId
);
return lineLocations;
}
async function getCorrespondingSources() {
function getCorrespondingSources(name) {
const sources = Object.values(app.store.getState().sources.sourceDetails.entities);
// TODO: Also look up source-mapped sources?
return sources.filter(s => s.url?.endsWith(name));
}
console.log(getCorrespondingSources("_app-96d565375e549a2c.js"));
}
/** ###########################################################################
* some things we want to play around with
* ##########################################################################*/
async function main() {
// const pointStruct = JSON.parse("{\"checkpoint\":12,\"progress\":35935,\"position\":{\"kind\":\"OnPop\",\"offset\":0,\"frameIndex\":4,\"functionId\":\"28:1758\"}}");
// // RUN-1576
// // http://admin/crash/controller/dff404d0-e114-4622-8daa-1c3340ba7833
// const pointStruct = {
// "progress": 24900203,
// "checkpoint": 226
// };
// await pauseAt(pointStruct);
// await getAllFrames();
// await getSelectedSourceId();
// await selectLocation(sourceId);
insertSourceBreakpoints();
}
main();
// const initTimer = setInterval(() => {
// console.log("initTimer checking...");
// if (window.app) {
// clearInterval(initTimer);
// main();
// }
// }, 100);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment