Last active
October 16, 2024 13:15
-
-
Save Domiii/8d6372af8bb267a9db96ec71a2d9d5ee to your computer and use it in GitHub Desktop.
Replay Devtools Breakpoint Visualizer + more snippets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** ########################################################################### | |
* Frontend Queries | |
* ##########################################################################*/ | |
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 InvalidCheckpointId = 0; | |
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; | |
} | |
} | |
function BigIntToPoint(n) { | |
const offset = readValue(32); | |
const kindValue = readValue(3); | |
const indexValue = readValue(24); | |
const hasPosition = readValue(1); | |
const progress = readValue(48); | |
const checkpoint = readValue(32) + FirstCheckpointId; | |
if (!hasPosition) { | |
if (offset) { | |
return { checkpoint, progress, bookmark: offset }; | |
} | |
return { checkpoint, progress }; | |
} | |
let kind; | |
switch (kindValue) { | |
case 0: | |
kind = "EnterFrame"; | |
break; | |
case 1: | |
kind = "OnStep"; | |
break; | |
case 2: | |
ThrowError("UnexpectedOnThrowPoint", { point: n.toString() + "n" }); | |
break; | |
case 3: | |
kind = "OnPop"; | |
break; | |
case 4: | |
kind = "OnUnwind"; | |
break; | |
} | |
const frameIndex = (1 << 24) - 1 - indexValue; | |
return { | |
checkpoint, | |
progress, | |
position: { kind, offset, frameIndex }, | |
}; | |
function readValue(nbits) { | |
const mask = (BigInt(1) << BigInt(nbits)) - BigInt(1); | |
const rv = Number(n & mask); | |
n = n >> BigInt(nbits); | |
return rv; | |
} | |
} | |
/** ########################################################################### | |
* 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 = { | |
// "checkpoint": 86, | |
// "progress": 44196840, | |
// "bookmark": 2149 | |
// }; | |
// await pauseAt(pointStruct); | |
// await getAllFrames(); | |
console.log(await getSelectedSourceId()); | |
// await selectLocation(sourceId); | |
insertSourceBreakpoints(); | |
// copy(BigIntToPoint(27584077111921759048236340451739749n)); | |
} | |
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