lionscope 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dashboard/assets/index-CAhmk3w_.js +15 -0
- package/dashboard/assets/index-DGwPvuPB.css +1 -0
- package/dashboard/index.html +2 -2
- package/dist/lionscope.mjs +280 -29
- package/package.json +1 -1
- package/dashboard/assets/index-Bh_BvZH_.css +0 -1
- package/dashboard/assets/index-DgtVKB-Z.js +0 -14
package/dist/lionscope.mjs
CHANGED
|
@@ -23533,6 +23533,8 @@ function isValidPackageName(value) {
|
|
|
23533
23533
|
return value.length <= 255 && PACKAGE_NAME_PATTERN.test(value);
|
|
23534
23534
|
}
|
|
23535
23535
|
var MAX_FRAME_DURATIONS = 240;
|
|
23536
|
+
var STALL_THRESHOLDS_MS = [100, 150, 250, 500, 1e3];
|
|
23537
|
+
var DEFAULT_SDK_OPTIONS = { stallThresholdMs: 150, strictMode: false };
|
|
23536
23538
|
var FRAME_STAGES = [
|
|
23537
23539
|
"input",
|
|
23538
23540
|
"animation",
|
|
@@ -23544,6 +23546,7 @@ var FRAME_STAGES = [
|
|
|
23544
23546
|
"delay",
|
|
23545
23547
|
"gpu"
|
|
23546
23548
|
];
|
|
23549
|
+
var MAX_STACK_FRAMES = 64;
|
|
23547
23550
|
var CONTROL_SERVER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
23548
23551
|
"hello",
|
|
23549
23552
|
"devices",
|
|
@@ -23553,8 +23556,9 @@ var CONTROL_SERVER_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
23553
23556
|
"error"
|
|
23554
23557
|
]);
|
|
23555
23558
|
var MAX_CLIENT_MESSAGE_BYTES = 4096;
|
|
23556
|
-
var DEVICE_PROTOCOL_VERSION =
|
|
23559
|
+
var DEVICE_PROTOCOL_VERSION = 2;
|
|
23557
23560
|
var MIN_DEVICE_PROTOCOL_VERSION = 1;
|
|
23561
|
+
var DEVICE_PROTOCOL_V2 = 2;
|
|
23558
23562
|
var MAX_DEVICE_PROTOCOL_VERSION = 1e3;
|
|
23559
23563
|
var DEVICE_LINK_PORT = DEFAULT_DEVICE_GATEWAY_PORT;
|
|
23560
23564
|
var MAX_DEVICE_LINE_BYTES = 64 * 1024;
|
|
@@ -23567,7 +23571,15 @@ var clientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
23567
23571
|
external_exports.object({ type: external_exports.literal("refreshDevices") }).strict(),
|
|
23568
23572
|
external_exports.object({ type: external_exports.literal("listApps"), serial: serialSchema }).strict(),
|
|
23569
23573
|
external_exports.object({ type: external_exports.literal("startSession"), serial: serialSchema, packageName: packageNameSchema }).strict(),
|
|
23570
|
-
external_exports.object({ type: external_exports.literal("stopSession") }).strict()
|
|
23574
|
+
external_exports.object({ type: external_exports.literal("stopSession") }).strict(),
|
|
23575
|
+
external_exports.object({
|
|
23576
|
+
type: external_exports.literal("setSdkOptions"),
|
|
23577
|
+
stallThresholdMs: external_exports.literal(STALL_THRESHOLDS_MS).optional(),
|
|
23578
|
+
strictMode: external_exports.boolean().optional()
|
|
23579
|
+
}).strict().refine(
|
|
23580
|
+
(message) => message.stallThresholdMs !== void 0 || message.strictMode !== void 0,
|
|
23581
|
+
"no SDK option given"
|
|
23582
|
+
)
|
|
23571
23583
|
]);
|
|
23572
23584
|
var utf8 = new TextEncoder();
|
|
23573
23585
|
function readJson(raw, maxBytes) {
|
|
@@ -23622,6 +23634,20 @@ var sdkFrameSchema = external_exports.object({
|
|
|
23622
23634
|
deadlineMs: durationMs.nullable(),
|
|
23623
23635
|
firstDraw: external_exports.boolean()
|
|
23624
23636
|
}).strip();
|
|
23637
|
+
var MAX_STACK_LINE = 2147483647;
|
|
23638
|
+
var MAX_STALL_DURATION_MS = 6e4;
|
|
23639
|
+
var MAX_STALL_SAMPLES = 1e4;
|
|
23640
|
+
var MAX_STRICT_MODE_COUNT = 1e6;
|
|
23641
|
+
var CONTROL_OR_FORMAT_RUNS = /[\p{Cc}\p{Cf}]+/gu;
|
|
23642
|
+
var sanitized = (value) => value.replace(CONTROL_OR_FORMAT_RUNS, " ").trim();
|
|
23643
|
+
var stackFrameSchema = external_exports.object({
|
|
23644
|
+
className: text,
|
|
23645
|
+
method: text,
|
|
23646
|
+
file: textOf(128).nullable(),
|
|
23647
|
+
// -2 marks a native method; -1 (unknown) is tolerated although the SDK sends null for it.
|
|
23648
|
+
line: external_exports.number().int().min(-2).max(MAX_STACK_LINE).nullable(),
|
|
23649
|
+
inApp: external_exports.boolean()
|
|
23650
|
+
}).strip();
|
|
23625
23651
|
var deviceMessageSchema = external_exports.discriminatedUnion("type", [
|
|
23626
23652
|
external_exports.object({
|
|
23627
23653
|
type: external_exports.literal("hello"),
|
|
@@ -23643,30 +23669,67 @@ var deviceMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
23643
23669
|
frames: external_exports.array(sdkFrameSchema).max(MAX_SDK_FRAMES_PER_BATCH),
|
|
23644
23670
|
droppedFrames: external_exports.number().int().nonnegative().max(MAX_SDK_DROPPED_FRAMES).optional()
|
|
23645
23671
|
}).strip(),
|
|
23646
|
-
external_exports.object({ type: external_exports.literal("heartbeat"), ts: epochMs, droppedMessages: external_exports.number().int().nonnegative() }).strip()
|
|
23672
|
+
external_exports.object({ type: external_exports.literal("heartbeat"), ts: epochMs, droppedMessages: external_exports.number().int().nonnegative() }).strip(),
|
|
23673
|
+
external_exports.object({
|
|
23674
|
+
type: external_exports.literal("stall"),
|
|
23675
|
+
ts: epochMs,
|
|
23676
|
+
durationMs: external_exports.number().nonnegative().max(MAX_STALL_DURATION_MS),
|
|
23677
|
+
frames: external_exports.array(stackFrameSchema).max(MAX_STACK_FRAMES),
|
|
23678
|
+
samples: external_exports.number().int().min(1).max(MAX_STALL_SAMPLES),
|
|
23679
|
+
truncated: external_exports.boolean(),
|
|
23680
|
+
screen: text.nullable(),
|
|
23681
|
+
debugger: external_exports.boolean()
|
|
23682
|
+
}).strip(),
|
|
23683
|
+
external_exports.object({
|
|
23684
|
+
type: external_exports.literal("strictmode"),
|
|
23685
|
+
ts: epochMs,
|
|
23686
|
+
policy: external_exports.enum(["thread", "vm"]),
|
|
23687
|
+
// Free text from the app's StrictMode violation: cleaned up rather than refused. An anonymous violation
|
|
23688
|
+
// class has an empty simple name.
|
|
23689
|
+
violation: external_exports.string().max(128).transform((value) => sanitized(value) || "Violation"),
|
|
23690
|
+
message: external_exports.string().max(MAX_SDK_TEXT_LENGTH).transform((value) => sanitized(value) || null).nullable(),
|
|
23691
|
+
frames: external_exports.array(stackFrameSchema).max(MAX_STACK_FRAMES),
|
|
23692
|
+
count: external_exports.number().int().min(1).max(MAX_STRICT_MODE_COUNT),
|
|
23693
|
+
screen: text.nullable()
|
|
23694
|
+
}).strip()
|
|
23647
23695
|
]);
|
|
23648
|
-
var
|
|
23649
|
-
|
|
23650
|
-
|
|
23651
|
-
|
|
23652
|
-
|
|
23696
|
+
var DIAGNOSTIC_TYPES = /* @__PURE__ */ new Set(["stall", "strictmode"]);
|
|
23697
|
+
var DEVICE_MESSAGE_VERSIONS = {
|
|
23698
|
+
hello: MIN_DEVICE_PROTOCOL_VERSION,
|
|
23699
|
+
screen: MIN_DEVICE_PROTOCOL_VERSION,
|
|
23700
|
+
frames: MIN_DEVICE_PROTOCOL_VERSION,
|
|
23701
|
+
heartbeat: MIN_DEVICE_PROTOCOL_VERSION,
|
|
23702
|
+
stall: DEVICE_PROTOCOL_V2,
|
|
23703
|
+
strictmode: DEVICE_PROTOCOL_V2
|
|
23653
23704
|
};
|
|
23705
|
+
function isKnownType(type) {
|
|
23706
|
+
return Object.hasOwn(DEVICE_MESSAGE_VERSIONS, type);
|
|
23707
|
+
}
|
|
23654
23708
|
function isSupportedProtocolVersion(version2) {
|
|
23655
23709
|
return Number.isInteger(version2) && version2 >= MIN_DEVICE_PROTOCOL_VERSION && version2 <= MAX_DEVICE_PROTOCOL_VERSION;
|
|
23656
23710
|
}
|
|
23657
|
-
function parseDeviceMessage(line) {
|
|
23711
|
+
function parseDeviceMessage(line, protocolVersion = DEVICE_PROTOCOL_VERSION) {
|
|
23658
23712
|
const json2 = readJson(line, MAX_DEVICE_LINE_BYTES);
|
|
23659
23713
|
if (!json2.ok) return json2;
|
|
23660
23714
|
const object2 = typeof json2.value === "object" && json2.value !== null ? json2.value : null;
|
|
23661
|
-
const
|
|
23662
|
-
|
|
23663
|
-
|
|
23715
|
+
const rawType = object2 && Reflect.get(object2, "type");
|
|
23716
|
+
let type = null;
|
|
23717
|
+
if (typeof rawType === "string") {
|
|
23718
|
+
if (!isKnownType(rawType)) return { ok: false, error: "unknown message type", unknownType: rawType };
|
|
23719
|
+
type = rawType;
|
|
23720
|
+
}
|
|
23721
|
+
const since = type === null ? MIN_DEVICE_PROTOCOL_VERSION : DEVICE_MESSAGE_VERSIONS[type];
|
|
23722
|
+
if (type !== null && since > protocolVersion) {
|
|
23723
|
+
return { ok: false, error: `${type} needs protocol version ${since}`, unnegotiated: { type, since } };
|
|
23664
23724
|
}
|
|
23665
23725
|
const version2 = object2 && type === "hello" ? Reflect.get(object2, "protocolVersion") : void 0;
|
|
23666
23726
|
if (typeof version2 === "number" && !isSupportedProtocolVersion(version2)) {
|
|
23667
23727
|
return { ok: false, error: `unsupported protocol version ${version2}`, unsupportedVersion: version2 };
|
|
23668
23728
|
}
|
|
23669
|
-
|
|
23729
|
+
const result = validate2(json2.value, deviceMessageSchema);
|
|
23730
|
+
if (!result.ok && type !== null && DIAGNOSTIC_TYPES.has(type))
|
|
23731
|
+
return { ...result, invalidDiagnostic: type };
|
|
23732
|
+
return result;
|
|
23670
23733
|
}
|
|
23671
23734
|
|
|
23672
23735
|
// server/src/config.ts
|
|
@@ -23870,6 +23933,8 @@ function createDeviceGateway(options) {
|
|
|
23870
23933
|
const warnings = new ErrorThrottle();
|
|
23871
23934
|
let address = null;
|
|
23872
23935
|
let ignoredMessages = 0;
|
|
23936
|
+
let unnegotiatedMessages = 0;
|
|
23937
|
+
let invalidDiagnostics = 0;
|
|
23873
23938
|
const warn = (reason) => {
|
|
23874
23939
|
if (warnings.shouldReport(reason)) log.warn(`Device gateway: closed a connection (${reason})`);
|
|
23875
23940
|
};
|
|
@@ -23881,6 +23946,20 @@ function createDeviceGateway(options) {
|
|
|
23881
23946
|
`Device gateway: ignoring SDK messages of unknown type "${shown}" (a newer SDK?), ${ignoredMessages} so far`
|
|
23882
23947
|
);
|
|
23883
23948
|
};
|
|
23949
|
+
const skipUnnegotiated = (type, since, negotiated) => {
|
|
23950
|
+
unnegotiatedMessages += 1;
|
|
23951
|
+
if (!warnings.shouldReport("unnegotiated type")) return;
|
|
23952
|
+
log.warn(
|
|
23953
|
+
`Device gateway: ignoring "${type}": SDK sent a v${since} message on a v${negotiated} link, ${unnegotiatedMessages} so far`
|
|
23954
|
+
);
|
|
23955
|
+
};
|
|
23956
|
+
const dropDiagnostic = (type, error62) => {
|
|
23957
|
+
invalidDiagnostics += 1;
|
|
23958
|
+
if (!warnings.shouldReport("invalid diagnostic")) return;
|
|
23959
|
+
log.warn(
|
|
23960
|
+
`Device gateway: dropped an invalid SDK ${type} message (${error62.slice(0, 200)}), ${invalidDiagnostics} so far`
|
|
23961
|
+
);
|
|
23962
|
+
};
|
|
23884
23963
|
const server = createServer((socket) => {
|
|
23885
23964
|
socket.on("error", () => void 0);
|
|
23886
23965
|
if (handshaking.size >= limits.maxHandshakes) {
|
|
@@ -23902,7 +23981,16 @@ function createDeviceGateway(options) {
|
|
|
23902
23981
|
linked.add(socket);
|
|
23903
23982
|
return null;
|
|
23904
23983
|
};
|
|
23905
|
-
serveConnection(socket, {
|
|
23984
|
+
serveConnection(socket, {
|
|
23985
|
+
target: options.target,
|
|
23986
|
+
limits,
|
|
23987
|
+
now,
|
|
23988
|
+
warn,
|
|
23989
|
+
ignore,
|
|
23990
|
+
skipUnnegotiated,
|
|
23991
|
+
dropDiagnostic,
|
|
23992
|
+
admit
|
|
23993
|
+
});
|
|
23906
23994
|
});
|
|
23907
23995
|
return {
|
|
23908
23996
|
get address() {
|
|
@@ -23928,18 +24016,26 @@ function createDeviceGateway(options) {
|
|
|
23928
24016
|
}
|
|
23929
24017
|
};
|
|
23930
24018
|
}
|
|
24019
|
+
function gatewayLine(message) {
|
|
24020
|
+
return `${JSON.stringify(message)}
|
|
24021
|
+
`;
|
|
24022
|
+
}
|
|
24023
|
+
function configMessage(options) {
|
|
24024
|
+
return { type: "config", stallThresholdMs: options.stallThresholdMs, strictMode: options.strictMode };
|
|
24025
|
+
}
|
|
23931
24026
|
function endSocket(socket, message) {
|
|
23932
|
-
if (message) socket.end(
|
|
23933
|
-
`);
|
|
24027
|
+
if (message) socket.end(gatewayLine(message));
|
|
23934
24028
|
else socket.end();
|
|
23935
24029
|
setTimeout(() => socket.destroy(), CLOSE_GRACE_MS).unref();
|
|
23936
24030
|
}
|
|
23937
24031
|
function serveConnection(socket, context) {
|
|
23938
|
-
const { target, limits, warn, ignore } = context;
|
|
24032
|
+
const { target, limits, warn, ignore, skipUnnegotiated, dropDiagnostic } = context;
|
|
23939
24033
|
const bucket = new TokenBucket(limits.lineBurst, limits.linesPerSecond, context.now);
|
|
23940
24034
|
let partial2 = [];
|
|
23941
24035
|
let partialBytes = 0;
|
|
23942
24036
|
let handler = null;
|
|
24037
|
+
let protocolVersion = 0;
|
|
24038
|
+
let pendingConfig = null;
|
|
23943
24039
|
let closing = false;
|
|
23944
24040
|
const close = (bye) => {
|
|
23945
24041
|
if (closing) return;
|
|
@@ -23967,13 +24063,23 @@ function serveConnection(socket, context) {
|
|
|
23967
24063
|
warn("too many SDK connections");
|
|
23968
24064
|
return close(refused);
|
|
23969
24065
|
}
|
|
23970
|
-
|
|
23971
|
-
const
|
|
24066
|
+
protocolVersion = Math.min(parsed.value.protocolVersion, DEVICE_PROTOCOL_VERSION);
|
|
24067
|
+
const link = {
|
|
24068
|
+
protocolVersion,
|
|
24069
|
+
close,
|
|
24070
|
+
sendConfig: (options) => {
|
|
24071
|
+
if (protocolVersion < DEVICE_PROTOCOL_V2 || closing) return;
|
|
24072
|
+
if (handler) socket.write(gatewayLine(configMessage(options)));
|
|
24073
|
+
else pendingConfig = configMessage(options);
|
|
24074
|
+
}
|
|
24075
|
+
};
|
|
24076
|
+
const result = target.attachSdk(parsed.value, link);
|
|
23972
24077
|
if (!result.ok) return close(result.reason);
|
|
23973
24078
|
handler = result.handler;
|
|
23974
|
-
|
|
23975
|
-
socket.write(
|
|
23976
|
-
|
|
24079
|
+
if (closing) return;
|
|
24080
|
+
socket.write(gatewayLine({ type: "welcome", protocolVersion }));
|
|
24081
|
+
if (pendingConfig) socket.write(gatewayLine(pendingConfig));
|
|
24082
|
+
pendingConfig = null;
|
|
23977
24083
|
};
|
|
23978
24084
|
const onLine = (raw) => {
|
|
23979
24085
|
if (!bucket.take()) {
|
|
@@ -23982,8 +24088,15 @@ function serveConnection(socket, context) {
|
|
|
23982
24088
|
}
|
|
23983
24089
|
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
23984
24090
|
if (!handler) return onHello(line);
|
|
23985
|
-
const parsed = parseDeviceMessage(line);
|
|
24091
|
+
const parsed = parseDeviceMessage(line, protocolVersion);
|
|
23986
24092
|
if (!parsed.ok && parsed.unknownType !== void 0) return ignore(parsed.unknownType);
|
|
24093
|
+
if (!parsed.ok && parsed.unnegotiated) {
|
|
24094
|
+
return skipUnnegotiated(parsed.unnegotiated.type, parsed.unnegotiated.since, protocolVersion);
|
|
24095
|
+
}
|
|
24096
|
+
if (!parsed.ok && parsed.invalidDiagnostic !== void 0) {
|
|
24097
|
+
dropDiagnostic(parsed.invalidDiagnostic, parsed.error);
|
|
24098
|
+
return handler.onInvalidDiagnostic();
|
|
24099
|
+
}
|
|
23987
24100
|
if (!parsed.ok || parsed.value.type === "hello") {
|
|
23988
24101
|
warn("invalid message");
|
|
23989
24102
|
return close("protocol_error");
|
|
@@ -24206,8 +24319,10 @@ var PUBLIC_FAILURES = {
|
|
|
24206
24319
|
refreshDevices: "Could not list devices",
|
|
24207
24320
|
listApps: "Could not list apps on the device",
|
|
24208
24321
|
startSession: "Could not start the profiling session",
|
|
24209
|
-
stopSession: "Could not stop the profiling session"
|
|
24322
|
+
stopSession: "Could not stop the profiling session",
|
|
24323
|
+
setSdkOptions: "Could not change the SDK options"
|
|
24210
24324
|
};
|
|
24325
|
+
var NO_SESSION_MESSAGE = "No profiling session: start one before changing SDK options";
|
|
24211
24326
|
function createWsGateway(options) {
|
|
24212
24327
|
const { server, manager } = options;
|
|
24213
24328
|
const wss = new import_websocket_server.default({ noServer: true, maxPayload: MAX_CLIENT_MESSAGE_BYTES });
|
|
@@ -24257,6 +24372,13 @@ function createWsGateway(options) {
|
|
|
24257
24372
|
case "stopSession":
|
|
24258
24373
|
send(ws, { type: "session", session: await manager.stopSession() });
|
|
24259
24374
|
return;
|
|
24375
|
+
case "setSdkOptions": {
|
|
24376
|
+
const { stallThresholdMs, strictMode } = message;
|
|
24377
|
+
if (!manager.setSdkOptions({ stallThresholdMs, strictMode })) {
|
|
24378
|
+
send(ws, { type: "error", code: "no_session", message: NO_SESSION_MESSAGE });
|
|
24379
|
+
}
|
|
24380
|
+
return;
|
|
24381
|
+
}
|
|
24260
24382
|
}
|
|
24261
24383
|
};
|
|
24262
24384
|
const onMessage = (ws, state, data, isBinary) => {
|
|
@@ -24561,6 +24683,12 @@ var TimeWindowBuffer = class {
|
|
|
24561
24683
|
// server/src/store/sessionStore.ts
|
|
24562
24684
|
var MAX_SAMPLES = 5e3;
|
|
24563
24685
|
var MAX_SCREEN_SPANS = 2e3;
|
|
24686
|
+
var MAX_EVENTS = 2e3;
|
|
24687
|
+
var MAX_EVENT_STREAM_BYTES = 2 * 1024 * 1024;
|
|
24688
|
+
var eventLimit = {
|
|
24689
|
+
maxBytes: MAX_EVENT_STREAM_BYTES,
|
|
24690
|
+
sizeOf: (item) => Buffer.byteLength(JSON.stringify(item))
|
|
24691
|
+
};
|
|
24564
24692
|
var STREAMS = {
|
|
24565
24693
|
cpu: { maxItems: MAX_SAMPLES, toMessages: (items) => items.map((sample) => ({ type: "cpu", sample })) },
|
|
24566
24694
|
memory: {
|
|
@@ -24586,6 +24714,16 @@ var STREAMS = {
|
|
|
24586
24714
|
maxItems: MAX_SAMPLES,
|
|
24587
24715
|
increasing: true,
|
|
24588
24716
|
toMessages: (items) => items.map((sample) => ({ type: "breakdown", sample }))
|
|
24717
|
+
},
|
|
24718
|
+
stalls: {
|
|
24719
|
+
maxItems: MAX_EVENTS,
|
|
24720
|
+
byteLimit: eventLimit,
|
|
24721
|
+
toMessages: (items) => items.map((sample) => ({ type: "stall", sample }))
|
|
24722
|
+
},
|
|
24723
|
+
strictMode: {
|
|
24724
|
+
maxItems: MAX_EVENTS,
|
|
24725
|
+
byteLimit: eventLimit,
|
|
24726
|
+
toMessages: (items) => items.map((sample) => ({ type: "strictmode", sample }))
|
|
24589
24727
|
}
|
|
24590
24728
|
};
|
|
24591
24729
|
var SAMPLE_STREAMS = Object.keys(STREAMS);
|
|
@@ -24607,7 +24745,9 @@ var SessionStore = class {
|
|
|
24607
24745
|
frames: buffer("frames"),
|
|
24608
24746
|
logs: buffer("logs"),
|
|
24609
24747
|
events: buffer("events"),
|
|
24610
|
-
breakdown: buffer("breakdown")
|
|
24748
|
+
breakdown: buffer("breakdown"),
|
|
24749
|
+
stalls: buffer("stalls"),
|
|
24750
|
+
strictMode: buffer("strictMode")
|
|
24611
24751
|
};
|
|
24612
24752
|
this.screens = new TimeWindowBuffer(options.windowMs, MAX_SCREEN_SPANS);
|
|
24613
24753
|
}
|
|
@@ -25148,12 +25288,19 @@ var MAX_FRAME_DURATION_MS = 6e4;
|
|
|
25148
25288
|
var MAX_HELD_MESSAGES = 16;
|
|
25149
25289
|
var droppedFramesMessage = (count) => `SDK reported ${count} dropped frames (device busy); frame counts may read low`;
|
|
25150
25290
|
var SDK_CLOCK_MESSAGE = "SDK frames dropped: the device clock does not match the laptop clock";
|
|
25291
|
+
var MAX_EVENT_AGE_MS = 6e4;
|
|
25292
|
+
var MAX_EVENT_LEAD_MS = MAX_FRAME_LEAD_MS;
|
|
25293
|
+
var INVALID_DIAGNOSTIC_MESSAGE = "SDK sent an invalid diagnostic; ignored";
|
|
25294
|
+
var CONFIG_DEBOUNCE_MS = 150;
|
|
25295
|
+
var SDK_EVENT_CLOCK_MESSAGE = "SDK stalls / StrictMode violations dropped: the device clock does not match the laptop clock";
|
|
25151
25296
|
var DISCONNECTED_SDK = {
|
|
25152
25297
|
connected: false,
|
|
25153
25298
|
sdkVersion: null,
|
|
25154
25299
|
processName: null,
|
|
25155
25300
|
apiLevel: null,
|
|
25156
|
-
framesActive: false
|
|
25301
|
+
framesActive: false,
|
|
25302
|
+
protocolVersion: null,
|
|
25303
|
+
options: DEFAULT_SDK_OPTIONS
|
|
25157
25304
|
};
|
|
25158
25305
|
var SdkBridge = class {
|
|
25159
25306
|
constructor(options) {
|
|
@@ -25170,13 +25317,33 @@ var SdkBridge = class {
|
|
|
25170
25317
|
/** Frames FrameMetrics dropped before the SDK saw them, summed over the session. */
|
|
25171
25318
|
droppedFrames = 0;
|
|
25172
25319
|
drainTimer = null;
|
|
25320
|
+
configTimer = null;
|
|
25173
25321
|
stopped = false;
|
|
25174
25322
|
get status() {
|
|
25175
25323
|
return this.current;
|
|
25176
25324
|
}
|
|
25325
|
+
/**
|
|
25326
|
+
* Change the SDK options (partial): announced with the status at once and, when they changed, pushed to the
|
|
25327
|
+
* links CONFIG_DEBOUNCE_MS after the last change.
|
|
25328
|
+
*/
|
|
25329
|
+
setOptions(changes) {
|
|
25330
|
+
if (this.stopped) return;
|
|
25331
|
+
const previous = this.sdkOptions;
|
|
25332
|
+
const options = {
|
|
25333
|
+
stallThresholdMs: changes.stallThresholdMs ?? previous.stallThresholdMs,
|
|
25334
|
+
strictMode: changes.strictMode ?? previous.strictMode
|
|
25335
|
+
};
|
|
25336
|
+
this.update({ options });
|
|
25337
|
+
if (options.stallThresholdMs === previous.stallThresholdMs && options.strictMode === previous.strictMode) {
|
|
25338
|
+
return;
|
|
25339
|
+
}
|
|
25340
|
+
if (this.configTimer) clearTimeout(this.configTimer);
|
|
25341
|
+
this.configTimer = setTimeout(() => this.pushConfig(), CONFIG_DEBOUNCE_MS);
|
|
25342
|
+
}
|
|
25177
25343
|
attach(hello, link) {
|
|
25178
25344
|
const entry = { hello, link, held: [] };
|
|
25179
25345
|
this.pending.add(entry);
|
|
25346
|
+
link.sendConfig(this.sdkOptions);
|
|
25180
25347
|
void this.verify(entry);
|
|
25181
25348
|
return {
|
|
25182
25349
|
onMessage: (message) => {
|
|
@@ -25184,12 +25351,17 @@ var SdkBridge = class {
|
|
|
25184
25351
|
if (this.active === entry) this.onMessage(message);
|
|
25185
25352
|
else if (this.pending.has(entry) && entry.held.length < MAX_HELD_MESSAGES) entry.held.push(message);
|
|
25186
25353
|
},
|
|
25354
|
+
onInvalidDiagnostic: () => {
|
|
25355
|
+
if (this.active === entry && !this.stopped) {
|
|
25356
|
+
this.options.notify("sdk:invalid-diagnostic", INVALID_DIAGNOSTIC_MESSAGE);
|
|
25357
|
+
}
|
|
25358
|
+
},
|
|
25187
25359
|
onClose: () => {
|
|
25188
25360
|
this.pending.delete(entry);
|
|
25189
25361
|
if (this.active !== entry || this.stopped) return;
|
|
25190
25362
|
this.active = null;
|
|
25191
25363
|
this.release();
|
|
25192
|
-
this.update({ connected: false, framesActive: false });
|
|
25364
|
+
this.update({ connected: false, framesActive: false, protocolVersion: null });
|
|
25193
25365
|
}
|
|
25194
25366
|
};
|
|
25195
25367
|
}
|
|
@@ -25197,13 +25369,20 @@ var SdkBridge = class {
|
|
|
25197
25369
|
stop() {
|
|
25198
25370
|
if (this.stopped) return;
|
|
25199
25371
|
this.stopped = true;
|
|
25372
|
+
if (this.configTimer) clearTimeout(this.configTimer);
|
|
25373
|
+
this.configTimer = null;
|
|
25200
25374
|
for (const entry of this.pending) entry.link.close("no_session");
|
|
25201
25375
|
this.pending.clear();
|
|
25202
25376
|
const active = this.active;
|
|
25203
25377
|
this.active = null;
|
|
25204
25378
|
this.release();
|
|
25205
25379
|
active?.link.close("no_session");
|
|
25206
|
-
this.current = { ...this.current, connected: false, framesActive: false };
|
|
25380
|
+
this.current = { ...this.current, connected: false, framesActive: false, protocolVersion: null };
|
|
25381
|
+
}
|
|
25382
|
+
pushConfig() {
|
|
25383
|
+
this.configTimer = null;
|
|
25384
|
+
const links = this.active ? [...this.pending, this.active] : [...this.pending];
|
|
25385
|
+
for (const entry of links) entry.link.sendConfig(this.sdkOptions);
|
|
25207
25386
|
}
|
|
25208
25387
|
async verify(entry) {
|
|
25209
25388
|
const { pid } = entry.hello;
|
|
@@ -25226,7 +25405,8 @@ var SdkBridge = class {
|
|
|
25226
25405
|
connected: true,
|
|
25227
25406
|
sdkVersion: entry.hello.sdkVersion,
|
|
25228
25407
|
processName: entry.hello.processName,
|
|
25229
|
-
apiLevel: entry.hello.apiLevel
|
|
25408
|
+
apiLevel: entry.hello.apiLevel,
|
|
25409
|
+
protocolVersion: entry.link.protocolVersion
|
|
25230
25410
|
});
|
|
25231
25411
|
for (const message of entry.held.splice(0)) this.onMessage(message);
|
|
25232
25412
|
}
|
|
@@ -25263,8 +25443,65 @@ var SdkBridge = class {
|
|
|
25263
25443
|
);
|
|
25264
25444
|
}
|
|
25265
25445
|
return;
|
|
25446
|
+
case "stall": {
|
|
25447
|
+
const ts = this.eventTime(message.ts, message.durationMs);
|
|
25448
|
+
if (ts === null) return;
|
|
25449
|
+
const { durationMs: durationMs2, frames, samples, truncated, debugger: debuggerAttached } = message;
|
|
25450
|
+
this.options.store.append("stalls", {
|
|
25451
|
+
ts,
|
|
25452
|
+
durationMs: durationMs2,
|
|
25453
|
+
frames,
|
|
25454
|
+
samples,
|
|
25455
|
+
truncated,
|
|
25456
|
+
screen: this.screenAt(ts, message.screen),
|
|
25457
|
+
debugger: debuggerAttached
|
|
25458
|
+
});
|
|
25459
|
+
return;
|
|
25460
|
+
}
|
|
25461
|
+
case "strictmode": {
|
|
25462
|
+
const ts = this.eventTime(message.ts, 0);
|
|
25463
|
+
if (ts === null) return;
|
|
25464
|
+
const { policy, violation, count, frames } = message;
|
|
25465
|
+
this.options.store.append("strictMode", {
|
|
25466
|
+
ts,
|
|
25467
|
+
policy,
|
|
25468
|
+
violation,
|
|
25469
|
+
message: message.message,
|
|
25470
|
+
frames,
|
|
25471
|
+
count,
|
|
25472
|
+
screen: this.screenAt(ts, message.screen)
|
|
25473
|
+
});
|
|
25474
|
+
return;
|
|
25475
|
+
}
|
|
25266
25476
|
}
|
|
25267
25477
|
}
|
|
25478
|
+
/**
|
|
25479
|
+
* Laptop time of a stall / StrictMode event, or null when it lies outside the accepted window (reported) or
|
|
25480
|
+
* ended before the session started (dropped quietly). One that straddles the session start begins at it, like
|
|
25481
|
+
* screen spans; its duration is kept.
|
|
25482
|
+
*/
|
|
25483
|
+
eventTime(deviceTs, durationMs2) {
|
|
25484
|
+
const now = this.options.now();
|
|
25485
|
+
const ts = this.toLaptop(deviceTs);
|
|
25486
|
+
const lead = ts - (now + MAX_EVENT_LEAD_MS);
|
|
25487
|
+
const age = ts + durationMs2 - (now - MAX_EVENT_AGE_MS);
|
|
25488
|
+
if (lead <= 0 && age >= 0) {
|
|
25489
|
+
const { startedAt } = this.options;
|
|
25490
|
+
return ts + durationMs2 < startedAt ? null : Math.max(startedAt, Math.min(ts, now));
|
|
25491
|
+
}
|
|
25492
|
+
if (this.refusals.shouldReport("event-clock")) {
|
|
25493
|
+
const skew = lead > 0 ? `${Math.round(lead)} ms ahead of` : `${Math.round(-age)} ms behind`;
|
|
25494
|
+
log.warn(
|
|
25495
|
+
`${this.options.logPrefix} dropped an SDK stall/StrictMode event ${skew} the accepted window (clock offset ${this.options.clockOffsetMs()} ms)`
|
|
25496
|
+
);
|
|
25497
|
+
}
|
|
25498
|
+
this.options.notify("sdk:event-clock", SDK_EVENT_CLOCK_MESSAGE);
|
|
25499
|
+
return null;
|
|
25500
|
+
}
|
|
25501
|
+
/** The SDK's own screen name, else the top screen of the recorded spans. */
|
|
25502
|
+
screenAt(ts, sdkScreen) {
|
|
25503
|
+
return sdkScreen ?? this.options.store.topScreenAt(ts);
|
|
25504
|
+
}
|
|
25268
25505
|
onFrames(message) {
|
|
25269
25506
|
if (message.droppedFrames) {
|
|
25270
25507
|
this.droppedFrames += message.droppedFrames;
|
|
@@ -25330,6 +25567,9 @@ var SdkBridge = class {
|
|
|
25330
25567
|
this.frames.reset();
|
|
25331
25568
|
if (this.current.framesActive) this.options.setGfxinfoPaused(false);
|
|
25332
25569
|
}
|
|
25570
|
+
get sdkOptions() {
|
|
25571
|
+
return this.current.options ?? DEFAULT_SDK_OPTIONS;
|
|
25572
|
+
}
|
|
25333
25573
|
update(changes) {
|
|
25334
25574
|
this.current = { ...this.current, ...changes };
|
|
25335
25575
|
this.options.onStatusChange();
|
|
@@ -25857,6 +26097,10 @@ var ProfilingSession = class {
|
|
|
25857
26097
|
snapshot() {
|
|
25858
26098
|
return this.store.snapshot();
|
|
25859
26099
|
}
|
|
26100
|
+
/** Change this session's SDK options (partial); the session is re-announced and live links get `config`. */
|
|
26101
|
+
setSdkOptions(changes) {
|
|
26102
|
+
this.sdk.setOptions(changes);
|
|
26103
|
+
}
|
|
25860
26104
|
/** Hand an SDK connection for this app to the session; it goes live once its PID checks out. */
|
|
25861
26105
|
attachSdk(hello, link) {
|
|
25862
26106
|
return this.sdk.attach(hello, link);
|
|
@@ -26119,6 +26363,13 @@ var SessionManager = class {
|
|
|
26119
26363
|
}
|
|
26120
26364
|
return { ok: true, handler: session.attachSdk(hello, link) };
|
|
26121
26365
|
}
|
|
26366
|
+
/** Change the SDK options of the running session (announced with a `session` broadcast). false: no session. */
|
|
26367
|
+
setSdkOptions(changes) {
|
|
26368
|
+
const session = this.session;
|
|
26369
|
+
if (!session || session.isStopped) return false;
|
|
26370
|
+
session.setSdkOptions(changes);
|
|
26371
|
+
return true;
|
|
26372
|
+
}
|
|
26122
26373
|
/** Poll `adb devices -l` every devicePollMs until close(). */
|
|
26123
26374
|
startDevicePolling() {
|
|
26124
26375
|
const poll = async () => {
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff)format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff)format("woff")}@font-face{font-family:IBM Plex Sans;font-style:normal;font-display:swap;font-weight:600;src:url(/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2)format("woff2"),url(/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff)format("woff")}@font-face{font-family:IBM Plex Mono;font-style:normal;font-display:swap;font-weight:500;src:url(/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2)format("woff2"),url(/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff)format("woff")}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--font-sans:"IBM Plex Sans", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;--font-mono:"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;--text-overline:.65625rem;--text-meta:.71875rem;--text-small:.78125rem;--text-body:.84375rem;--text-title:1rem;--text-readout:1.625rem;--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:24px;--space-6:32px;--radius-s:3px;--radius-m:5px;--radius-pill:999px;--ease-out:cubic-bezier(.16, 1, .3, 1);--dur-fast:.12s;--dur:.22s;--plane:#eceef1;--surface:#fbfbfc;--surface-sunk:#f3f4f6;--surface-raised:#fff;--hover-wash:#1114180b;--active-wash:#11141814;--ink:#111418;--ink-2:#474e5a;--ink-muted:#5f6673;--ink-inverse:#fbfbfc;--rule:#e2e4e8;--rule-strong:#c4c8cf;--focus:#2a6fdb;--brand:#d7262f;--event:#6250d6;--event-wash:#6250d61a;--jank:#c26a00;--jank-text:#9a5200;--jank-wash:#c26a001a;--good:#0b7d32;--good-wash:#0b7d321a;--critical:#c42f2f;--critical-wash:#c42f2f14;--viz-cpu:#2a78d6;--viz-fps:#2a78d6;--viz-frametime:#2a78d6;--viz-framemax:#8b919c;--viz-mem-native:#1baf7a;--viz-mem-java:#2a78d6;--viz-mem-graphics:#e87ba4;--viz-mem-code:#008300;--viz-mem-other:#8b919c;--viz-mem-total:#111418;--viz-threshold:#5f6673;--viz-stage-input:#1c5cab;--viz-stage-animation:#2a78d6;--viz-stage-layout:#5598e7;--viz-stage-draw:#86b6ef;--viz-stage-sync:#4fb94a;--viz-stage-command:#2b9927;--viz-stage-swap:#007a00;--viz-stage-delay:#babfc7;--viz-stage-gpu:#e87ba4;--viz-screen-1:#2a78d6;--viz-screen-2:#e87ba4;--viz-screen-3:#008300;--viz-screen-wash:.06;--log-v:#5f6673;--log-d:#3f4752;--log-i:#1f6fbf;--log-w:#8f5a00;--log-e:#c42f2f;--log-f:#a3122b;--shadow-pop:0 1px 0 #1114180a, 0 14px 36px -10px #11141838}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--plane:#0b0d10;--surface:#13161b;--surface-sunk:#0f1216;--surface-raised:#1a1e24;--hover-wash:#e9ebee0d;--active-wash:#e9ebee17;--ink:#e9ebee;--ink-2:#b1b7c1;--ink-muted:#8a919c;--ink-inverse:#0b0d10;--rule:#232831;--rule-strong:#343a45;--focus:#6b9cf0;--brand:#f0434b;--event:#9085e9;--event-wash:#9085e924;--jank:#f0a420;--jank-text:#f3b23a;--jank-wash:#f0a4201f;--good:#3cc26a;--good-wash:#3cc26a1f;--critical:#f07070;--critical-wash:#f070701a;--viz-cpu:#3987e5;--viz-fps:#3987e5;--viz-frametime:#3987e5;--viz-framemax:#6f7682;--viz-mem-native:#199e70;--viz-mem-java:#3987e5;--viz-mem-graphics:#d55181;--viz-mem-code:#008300;--viz-mem-other:#6f7682;--viz-mem-total:#e9ebee;--viz-threshold:#8a919c;--viz-stage-input:#256abf;--viz-stage-animation:#3987e5;--viz-stage-layout:#6da7ec;--viz-stage-draw:#9ec5f4;--viz-stage-sync:#56c050;--viz-stage-command:#33a02e;--viz-stage-swap:green;--viz-stage-delay:#4a505b;--viz-stage-gpu:#d55181;--viz-screen-1:#3987e5;--viz-screen-2:#d55181;--viz-screen-3:#008300;--viz-screen-wash:.085;--log-v:#8a919c;--log-d:#c9ced6;--log-i:#6aa8f0;--log-w:#e8b04a;--log-e:#f07070;--log-f:#ff6b81;--shadow-pop:0 0 0 1px #e9ebee0f, 0 18px 40px -12px #000000b3}}*,:before,:after{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--plane);color:var(--ink);font-family:var(--font-sans);font-size:var(--text-body);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;line-height:1.45}#root{min-height:100dvh}button,input,select{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}code,kbd{font-family:var(--font-mono);font-size:.95em}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.overline{font-size:var(--text-overline);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-muted);font-weight:600}.mono{font-family:var(--font-mono);font-variant-numeric:tabular-nums}.btn{justify-content:center;align-items:center;gap:var(--space-2);height:30px;padding:0 var(--space-3);border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);font-size:var(--text-small);white-space:nowrap;transition:background-color var(--dur-fast) linear, border-color var(--dur-fast) linear, transform var(--dur-fast) var(--ease-out);font-weight:500;display:inline-flex}.btn:hover:not(:disabled){background:var(--hover-wash);border-color:var(--ink-muted)}.btn:active:not(:disabled){background:var(--active-wash);transform:translateY(1px)}.btn:disabled{opacity:.45}.btn--primary{background:var(--ink);border-color:var(--ink);color:var(--ink-inverse)}.btn--primary:hover:not(:disabled){background:var(--ink-2);border-color:var(--ink-2)}.btn--icon{width:30px;padding:0}.btn svg{flex:none}.segmented{border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);display:inline-flex;overflow:hidden}.segmented>button{min-width:34px;height:26px;padding:0 var(--space-2);border:0;border-left:1px solid var(--rule);font-family:var(--font-mono);font-size:var(--text-meta);color:var(--ink-2);transition:background-color var(--dur-fast) linear, color var(--dur-fast) linear;background:0 0;font-weight:500}.segmented>button:first-child{border-left:0}.segmented>button:hover:not([aria-checked=true]):not([aria-pressed=true]){background:var(--hover-wash);color:var(--ink)}.segmented>button[aria-checked=true],.segmented>button[aria-pressed=true]{background:var(--ink);color:var(--ink-inverse)}.segmented>button:focus-visible{outline-offset:-2px}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}.logcat{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex;container:logcat/inline-size}.logcat-head{gap:var(--space-2);padding:var(--space-3) var(--space-4) var(--space-3);border-bottom:1px solid var(--rule);display:grid}.logcat-title-row{align-items:baseline;gap:var(--space-2);display:flex}.logcat-title{font-size:var(--text-small);letter-spacing:.01em;margin:0;font-weight:600}.logcat-pid{color:var(--ink-muted);font-size:var(--text-meta)}.logcat-count{color:var(--ink-2);font-size:var(--text-meta);margin-left:auto}.logcat-count-unit{color:var(--ink-muted)}.logcat-clear{height:24px;padding:0 var(--space-2);font-size:var(--text-meta);align-self:center}.logcat-filters{gap:var(--space-2);flex-wrap:wrap;display:flex}.level-filter>button:not([data-included]){color:var(--ink-muted);text-decoration:line-through;-webkit-text-decoration-color:var(--rule-strong);text-decoration-color:var(--rule-strong)}.level-filter>button[data-included]:not([aria-checked=true]){background:var(--surface-sunk)}.logcat-search{min-width:0;color:var(--ink-muted);flex:160px;align-items:center;display:flex;position:relative}.logcat-search svg{pointer-events:none;position:absolute;left:9px}.logcat-search input{width:100%;height:28px;padding:0 var(--space-2) 0 28px;border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-sunk);color:var(--ink);font-family:var(--font-mono);font-size:var(--text-meta);transition:border-color var(--dur-fast) linear, background-color var(--dur-fast) linear}.logcat-search input::placeholder{color:var(--ink-muted)}.logcat-search input:hover{border-color:var(--ink-muted)}.logcat-search input:focus-visible{border-color:var(--focus);background:var(--surface);outline-offset:0}.logcat-body{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative}.logcat-list{min-height:0;padding:var(--space-1) 0;overscroll-behavior:contain;font-family:var(--font-mono);font-size:var(--text-meta);scrollbar-color:var(--rule-strong) transparent;flex:auto;margin:0;line-height:1.5;list-style:none;overflow-y:auto}.log-row{padding:1px var(--space-4) 1px calc(var(--space-4) - 2px);border-left:2px solid #0000;grid-template-columns:12.5ch 1.5ch minmax(0,15ch) minmax(0,1fr);column-gap:1.25ch;display:grid}.log-row:hover{background:var(--hover-wash)}.log-time{color:var(--ink-muted);font-variant-numeric:tabular-nums;white-space:nowrap}.log-level{text-align:center;font-weight:600}.log-tag{color:var(--ink-2);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.log-msg{color:var(--ink);white-space:pre-wrap;overflow-wrap:anywhere}.log-row mark{background:var(--event-wash);box-shadow:0 0 0 1px var(--event-wash);color:inherit;border-radius:2px}.log-row[data-level=V] .log-level{color:var(--log-v)}.log-row[data-level=V] .log-msg{color:var(--ink-2)}.log-row[data-level=D] .log-level{color:var(--log-d)}.log-row[data-level=I] .log-level{color:var(--log-i)}.log-row[data-level=W]{border-left-color:var(--jank);background:var(--jank-wash)}.log-row[data-level=W] .log-level{color:var(--log-w)}.log-row[data-level=E],.log-row[data-level=F]{border-left-color:var(--critical);background:var(--critical-wash)}.log-row[data-level=E] .log-level{color:var(--log-e)}.log-row[data-level=F] .log-level{color:var(--log-f)}.log-row[data-level=F] .log-msg{font-weight:500}.logcat-empty{padding:var(--space-5);color:var(--ink-muted);font-size:var(--text-small);text-align:center;flex:1;place-content:center;display:grid}.logcat-empty p{max-width:32ch;margin:0}.logcat-empty-title{color:var(--ink-2);font-weight:600;margin-bottom:var(--space-1)!important}.jump-latest{bottom:var(--space-3);height:28px;padding:0 var(--space-3);border:1px solid var(--ink);border-radius:var(--radius-pill);background:var(--ink);box-shadow:var(--shadow-pop);color:var(--ink-inverse);font-size:var(--text-meta);animation:jump-in var(--dur) var(--ease-out) both;align-items:center;gap:6px;font-weight:500;display:inline-flex;position:absolute;left:50%;transform:translate(-50%)}.jump-latest:hover{background:var(--ink-2)}.jump-count{border-left:1px solid color-mix(in srgb, var(--ink-inverse) 35%, transparent);padding-left:6px}@keyframes jump-in{0%{opacity:0;transform:translate(-50%,6px)}}@container logcat (width<=560px){.log-row{grid-template-columns:12.5ch 1.5ch minmax(0,1fr);padding-block:2px}.log-msg{grid-column:2/-1}}.screens{background:var(--surface);flex-direction:column;flex:auto;min-width:0;min-height:0;display:flex;container:screens/inline-size}.screens-head{justify-content:space-between;align-items:center;gap:var(--space-2);min-height:44px;padding:var(--space-2) var(--space-4);border-bottom:1px solid var(--rule);flex-wrap:wrap;display:flex}.screens-scope{align-items:baseline;gap:var(--space-2);color:var(--ink-2);font-size:var(--text-meta);margin:0;display:inline-flex}.screens-filter{max-width:100%;height:26px;padding:0 var(--space-2);border-color:var(--ink);font-size:var(--text-meta)}.screens-filter-name{text-overflow:ellipsis;font-weight:600;overflow:hidden}.screens-filter-hint{color:var(--ink-muted)}.screens-body{flex:auto;min-height:0;overflow:auto}.screens-table{border-collapse:collapse;width:100%;font-size:var(--text-small)}.screens-table thead th{z-index:1;border-bottom:1px solid var(--rule-strong);background:var(--surface);color:var(--ink-muted);font-size:var(--text-overline);letter-spacing:.06em;text-align:right;text-transform:uppercase;white-space:nowrap;padding:0;font-weight:600;position:sticky;top:0}.screens-table thead th[data-col=name],.screens-table thead th[data-col=dominant]{text-align:left}.screens-table thead th[data-col=dominant]>span{padding:8px var(--space-4) 7px var(--space-2);display:inline-block}.sort-btn{padding:8px var(--space-2) 7px;color:inherit;font:inherit;letter-spacing:inherit;text-transform:inherit;transition:color var(--dur-fast) linear;background:0 0;border:0;align-items:center;gap:4px;display:inline-flex}th[data-col=name] .sort-btn{padding-left:var(--space-4)}.sort-btn:hover{color:var(--ink)}.sort-btn svg{opacity:0;transition:opacity var(--dur-fast) linear, transform var(--dur) var(--ease-out)}th[aria-sort] .sort-btn{color:var(--ink)}th[aria-sort] .sort-btn svg{opacity:1}th[aria-sort=ascending] .sort-btn svg{transform:rotate(180deg)}.screens-table tbody tr{border-bottom:1px solid var(--rule);cursor:pointer;transition:background-color var(--dur-fast) linear}.screens-table tbody tr:hover{background:var(--hover-wash)}.screens-table tbody tr[data-selected]{background:var(--active-wash);box-shadow:inset 2px 0 0 var(--ink)}.screens-table tbody th,.screens-table td{padding:7px var(--space-2);text-align:right;vertical-align:middle;white-space:nowrap;font-weight:400}.screens-table tbody th{width:40%;max-width:0;padding-left:var(--space-4);text-align:left}.screens-table td.mono{font-size:var(--text-meta)}.screen-cell{max-width:100%;color:var(--ink);font:inherit;text-align:left;background:0 0;border:0;align-items:center;gap:7px;padding:0;font-weight:500;display:inline-flex}.screen-cell[data-none]{color:var(--ink-muted);font-family:var(--font-mono);font-size:var(--text-meta)}.screen-cell-name{text-overflow:ellipsis;overflow:hidden}.screen-cell:focus-visible{outline-offset:3px}td[data-col=jankPercent]{min-width:64px}.jank-value[data-flag]{color:var(--jank-text);font-weight:600}.jank-meter{background:var(--rule);border-radius:2px;height:3px;margin-top:3px;display:block;overflow:hidden}.jank-meter>span{background:var(--jank);transform-origin:0;height:100%;transition:transform var(--dur) var(--ease-out);border-radius:2px;display:block}.p90-worst{color:var(--ink-muted);font-size:var(--text-overline);margin-top:2px;display:block}td[data-col=dominant]{padding-right:var(--space-4);text-align:left}.dominant{font-size:var(--text-meta);align-items:center;gap:6px;display:inline-flex}.dominant-name{color:var(--ink-2)}.stage-profile{gap:var(--space-2);padding:var(--space-3) var(--space-4) var(--space-4);animation:profile-in var(--dur) var(--ease-out) both;margin:0;display:grid}@keyframes profile-in{0%{opacity:0;transform:translateY(3px)}}.stage-bar{gap:2px;height:10px;display:flex}.stage-seg{background:var(--key,var(--ink-muted));border-radius:2px;min-width:2px}.stage-list{font-size:var(--text-meta);gap:3px;margin:0;padding:0;list-style:none;display:grid}.stage-list li{align-items:center;gap:var(--space-2);grid-template-columns:auto 1fr auto 4ch;display:grid}.stage-name{color:var(--ink-2)}.stage-share{color:var(--ink-muted);text-align:right}.screens-note{padding:var(--space-3) var(--space-4);color:var(--ink-2);font-size:var(--text-small);margin:0}.screens-empty{gap:var(--space-2);max-width:560px;padding:var(--space-5) var(--space-4);color:var(--ink-2);font-size:var(--text-small);grid-template-columns:minmax(0,1fr);display:grid}.screens-empty p{margin:0}.screens-empty-title{color:var(--ink);font-size:var(--text-body);font-weight:600}.screens-empty[data-tone=critical] .screens-empty-title{color:var(--log-e)}.screens-empty-detail{padding:var(--space-2) var(--space-3);border-left:2px solid var(--critical);border-radius:var(--radius-s);background:var(--critical-wash);color:var(--ink);font-size:var(--text-meta);overflow-wrap:anywhere}.screens-empty .screens-empty-note{color:var(--ink-muted);font-size:var(--text-meta)}.screens-snippet{margin:var(--space-1) 0 0;padding:var(--space-2) var(--space-3);border:1px solid var(--rule);border-radius:var(--radius-s);background:var(--surface-sunk);color:var(--ink);font-size:var(--text-meta);line-height:1.55;overflow-x:auto}@container screens (width<=560px){.screens-table [data-col=slowPercent]{display:none}}@container screens (width<=440px){.screens-table [data-col=typicalP90Ms]{display:none}}.side{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex}.side-tabs{gap:var(--space-1);padding:0 var(--space-3);border-bottom:1px solid var(--rule);background:var(--surface);flex:none;display:flex}.side-tab{height:36px;padding:0 var(--space-2);color:var(--ink-muted);font-size:var(--text-small);letter-spacing:.01em;transition:color var(--dur-fast) linear;background:0 0;border:0;align-items:center;gap:6px;font-weight:600;display:inline-flex;position:relative}.side-tab:after{right:var(--space-2);bottom:-1px;left:var(--space-2);background:var(--ink);content:"";height:2px;transition:transform var(--dur) var(--ease-out);border-radius:1px;position:absolute;transform:scaleX(0)}.side-tab:hover,.side-tab[aria-selected=true]{color:var(--ink)}.side-tab[aria-selected=true]:after{transform:scaleX(1)}.side-tab:focus-visible{outline-offset:-2px}.side-tab-count{border-radius:var(--radius-pill);background:var(--surface-sunk);min-width:18px;box-shadow:inset 0 0 0 1px var(--rule-strong);color:var(--ink-2);font-size:var(--text-overline);text-align:center;padding:0 5px;font-weight:500;line-height:16px}.side-panel{flex-direction:column;flex:auto;min-width:0;min-height:0;display:flex}.side-panel:focus-visible{outline-offset:-2px}.side-panel[hidden]{display:none}.side-panel>.logcat{flex:auto}.stat-strip{border-bottom:1px solid var(--rule)}.readouts{grid-template-columns:repeat(6,minmax(0,1fr));margin:0;display:grid}.readout{min-width:0;padding:10px var(--space-4) 9px;border-right:1px solid var(--rule)}.readout:last-child{border-right:0}.readout .overline{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.readout-body{align-items:baseline;gap:2px var(--space-2);flex-wrap:wrap;margin:2px 0 0;display:flex}.readout-value{font-size:var(--text-readout);letter-spacing:-.02em;font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:500;line-height:1.05}.readout[data-empty] .readout-value{color:var(--ink-muted);font-weight:400}.readout-note{color:var(--ink-2);font-size:var(--text-body);white-space:nowrap;align-self:center;margin:5px 0 4px;font-weight:500}.readout[title]{cursor:help}.readout-unit{color:var(--ink-muted);font-size:var(--text-small);letter-spacing:0;margin-left:3px;font-weight:400}.readout-detail{text-overflow:ellipsis;color:var(--ink-muted);font-size:var(--text-meta);white-space:nowrap;flex-basis:100%;margin-top:1px;overflow:hidden}.readout-flag{border-radius:var(--radius-s);background:var(--jank-wash);height:18px;color:var(--jank-text);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;animation:flag-in var(--dur) var(--ease-out) both;align-items:center;gap:4px;padding:0 6px;font-weight:600;display:inline-flex}.readout-flag svg{color:var(--jank)}@keyframes flag-in{0%{opacity:0;transform:scale(.92)}}.cursor-readout{margin-right:var(--space-2);font-size:var(--text-small);white-space:nowrap;align-items:baseline;gap:6px;display:inline-flex}.live-btn{border-color:var(--ink)}.live-dot{background:var(--good);border-radius:50%;width:7px;height:7px}@media (width<=1400px){.readout{padding-inline:var(--space-3)}.readout-value{font-size:1.375rem}}@media (width<=760px){.readouts{grid-template-columns:repeat(3,minmax(0,1fr))}.readout{border-bottom:1px solid var(--rule)}.readout:nth-child(3n){border-right:0}.readout:nth-child(n+4){border-bottom:0}}@media (width<=420px){.readouts{grid-template-columns:repeat(2,minmax(0,1fr))}.readout:nth-child(3n){border-right:1px solid var(--rule)}.readout:nth-child(2n){border-right:0}.readout:nth-child(n+4){border-bottom:1px solid var(--rule)}.readout:nth-child(n+5){border-bottom:0}}.empty-state{max-width:440px;padding:var(--space-5) var(--space-5) var(--space-4);border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);box-shadow:var(--shadow-pop);color:var(--ink-2);font-size:var(--text-body);position:relative}.empty-state:before{border-radius:var(--radius-m) 0 0 var(--radius-m);background:var(--ink);content:"";width:3px;position:absolute;inset:-1px auto -1px -1px}.empty-state[data-tone=critical]:before{background:var(--critical)}.empty-state p{margin:0 0 var(--space-2)}.empty-state .overline{margin-bottom:var(--space-1)}.empty-title{margin:0 0 var(--space-3);color:var(--ink);font-size:var(--text-title);letter-spacing:-.005em;font-weight:600}.empty-code{margin:var(--space-2) 0 var(--space-1);padding:var(--space-2) var(--space-3);border:1px solid var(--rule);border-radius:var(--radius-s);background:var(--surface-sunk);color:var(--ink);font-family:var(--font-mono);font-size:var(--text-small)}.empty-state code{border-radius:var(--radius-s);background:var(--surface-sunk);color:var(--ink);padding:1px 4px}.empty-steps{gap:var(--space-2);counter-reset:step;margin:0;padding:0;list-style:none;display:grid}.empty-steps li{gap:var(--space-2);counter-increment:step;grid-template-columns:20px 1fr;display:grid}.empty-steps li:before{color:var(--ink-muted);font-family:var(--font-mono);font-size:var(--text-small);content:counter(step, decimal-leading-zero)}.empty-devices{gap:var(--space-2);margin:0;padding:0;list-style:none;display:grid}.empty-devices li{padding-left:var(--space-3);border-left:2px solid var(--jank);gap:2px;display:grid}.empty-devices .mono{color:var(--ink);font-size:var(--text-small)}.empty-error{padding:var(--space-2) var(--space-3);border-radius:var(--radius-s);background:var(--critical-wash);color:var(--log-e);font-size:var(--text-small);overflow-wrap:anywhere}.timeline-overlay{background-color:color-mix(in srgb, var(--surface) 82%, transparent);background-image:radial-gradient(circle, var(--rule-strong) 1px, transparent 1.2px);background-size:16px 16px}.banner{align-items:center;gap:var(--space-1) var(--space-2);padding:7px var(--space-4);border-bottom:1px solid var(--rule);background:var(--surface);font-size:var(--text-small);animation:banner-in var(--dur) var(--ease-out) both;flex-wrap:wrap;display:flex}.banner[data-tone=critical]{border-bottom-color:color-mix(in srgb, var(--critical) 35%, var(--rule));background:color-mix(in srgb, var(--critical) 7%, var(--surface))}.banner[data-tone=quiet]{color:var(--ink-2)}.banner code{font-size:var(--text-meta)}.banner-meta{color:var(--ink-muted);font-size:var(--text-meta)}.banner-hint{color:var(--ink-2)}.banner-dot{background:var(--critical);border-radius:50%;width:8px;height:8px}.banner-spinner{border:1.5px solid var(--rule-strong);border-top-color:var(--ink);border-radius:50%;width:10px;height:10px;animation:.9s linear infinite spin}.banner-close{border-radius:var(--radius-s);width:24px;height:24px;color:var(--ink-2);background:0 0;border:0;place-items:center;margin-left:auto;display:grid}.banner-close:hover{background:var(--hover-wash);color:var(--ink)}@keyframes banner-in{0%{opacity:0;transform:translateY(-4px)}}@keyframes spin{to{transform:rotate(360deg)}}.crash{min-height:100dvh;padding:var(--space-5) var(--space-4);place-items:center;display:grid}.crash-actions{gap:var(--space-2);margin-top:var(--space-3);display:flex}.uplot,.uplot *,.uplot :before,.uplot :after{box-sizing:border-box}.uplot{width:min-content;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{-webkit-user-select:none;user-select:none;position:relative}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{width:100%;height:100%;display:block;position:relative}.u-axis{position:absolute}.u-legend{text-align:center;margin:auto;font-size:14px}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{pointer-events:none;background:#00000012;position:absolute}.u-cursor-x,.u-cursor-y{pointer-events:none;will-change:transform;position:absolute;top:0;left:0}.u-hz .u-cursor-x,.u-vt .u-cursor-y{border-right:1px dashed #607d8b;height:100%}.u-hz .u-cursor-y,.u-vt .u-cursor-x{border-bottom:1px dashed #607d8b;width:100%}.u-cursor-pt{pointer-events:none;will-change:transform;border:0 solid;border-radius:50%;position:absolute;top:0;left:0;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}.lane.lane--screens{z-index:4;flex:none;height:auto;min-height:0;position:relative}.lane.lane--screens .lane-head{padding-bottom:2px}.screen-readout{min-width:0;font-size:var(--text-meta);white-space:nowrap;align-items:center;gap:6px;margin:0;display:inline-flex}.screen-readout-name{color:var(--ink);text-overflow:ellipsis;font-weight:500;overflow:hidden}.screen-readout[data-at-cursor] .legend-value{background:var(--hover-wash)}.screen-swatch{background:var(--band,var(--ink-muted));border-radius:2px;flex:none;width:9px;height:9px;display:inline-block}.screen-swatch[data-bg]{background:repeating-linear-gradient(135deg, var(--ink-muted) 0 1px, transparent 1px 3px);box-shadow:inset 0 0 0 1px var(--rule-strong)}[data-color=screen1]{--band:var(--viz-screen-1)}[data-color=screen2]{--band:var(--viz-screen-2)}[data-color=screen3]{--band:var(--viz-screen-3)}.screen-clear{max-width:220px;font-family:var(--font-sans);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:5px;display:inline-flex;overflow:hidden}.screen-clear-name{text-overflow:ellipsis;min-width:0;overflow:hidden}.screen-clear svg{opacity:.7;flex:none}.screen-view-link{border-radius:var(--radius-s);color:var(--ink-2);font-size:var(--text-meta);text-decoration:underline;-webkit-text-decoration-color:var(--rule-strong);text-decoration-color:var(--rule-strong);text-underline-offset:3px;white-space:nowrap;transition:color var(--dur-fast) linear, text-decoration-color var(--dur-fast) linear;background:0 0;border:0;align-self:center;align-items:center;gap:4px;padding:0 2px;font-weight:500;display:inline-flex}.screen-view-link:hover{color:var(--ink);-webkit-text-decoration-color:var(--ink);text-decoration-color:var(--ink)}.screen-view-link svg{transition:transform var(--dur) var(--ease-out)}.screen-view-link:hover svg{transform:translate(2px)}.screen-track-host{height:30px;position:relative}.screen-track{position:absolute;top:3px;bottom:5px}.screen-track:before{border-radius:var(--radius-s);background:var(--surface-sunk);content:"";position:absolute;inset:0}.screen-track-empty{padding-left:var(--space-2);color:var(--ink-muted);font-size:var(--text-meta);align-items:center;display:flex;position:absolute;inset:0}.screen-band{border:0;border-right:2px solid var(--surface);background:color-mix(in srgb, var(--band,var(--ink-muted)) 17%, var(--surface));min-width:3px;box-shadow:inset 2px 0 0 var(--band,var(--ink-muted));color:var(--ink);font-family:var(--font-sans);font-size:var(--text-meta);text-align:left;white-space:nowrap;transition:background-color var(--dur-fast) linear, opacity var(--dur-fast) linear;background-clip:padding-box;border-radius:2px;align-items:center;margin:0;padding:0;font-weight:500;line-height:1;display:flex;position:absolute;top:0;bottom:0;overflow:hidden}.screen-band[data-labelled]{padding:0 6px 0 7px}.screen-band-label{text-overflow:ellipsis;min-width:0;overflow:hidden}.screen-band[data-cluster]{background:repeating-linear-gradient(90deg, var(--ink-muted) 0 1px, transparent 1px 3px);box-shadow:none;opacity:.55;background-clip:padding-box}button.screen-band:hover{background-color:color-mix(in srgb, var(--band) 28%, var(--surface))}button.screen-band:focus-visible{z-index:2;outline-offset:0}.screen-band[data-open]{border-right:0;border-radius:2px 0 0 2px}.screen-band[data-selected]{z-index:1;background-color:color-mix(in srgb, var(--band) 34%, var(--surface));box-shadow:inset 3px 0 0 var(--band), inset 0 0 0 1px var(--band);font-weight:600}.screen-track[data-has-selection] .screen-band:not([data-selected]){opacity:.5}.screen-track[data-has-selection] .screen-band:not([data-selected]):hover{opacity:.85}.screen-band[data-bg]{background:repeating-linear-gradient(135deg, var(--rule-strong) 0 1px, color-mix(in srgb, var(--surface-sunk) 70%, var(--surface)) 1px 6px);box-shadow:none;color:var(--ink-muted);font-family:var(--font-mono);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;background-clip:padding-box;font-weight:500}.screen-track-cursor,.screen-track-probe{z-index:3;opacity:0;pointer-events:none;width:0;position:absolute;top:-3px;bottom:-5px;left:0}.screen-track-cursor{border-left:1px solid var(--ink-muted)}.screen-track-probe{border-left:1px solid var(--ink);transition:opacity var(--dur-fast) linear}.screen-tip{z-index:20;border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-raised);width:260px;box-shadow:var(--shadow-pop);pointer-events:none;animation:tip-in .14s var(--ease-out) both;padding:8px 10px 9px;position:absolute;top:calc(100% + 8px);transform:translate(-50%)}@keyframes tip-in{0%{opacity:0;transform:translate(-50%,-2px)}}.screen-tip p{margin:0}.screen-tip-title{align-items:center;gap:6px;min-width:0;display:flex}.screen-tip-name{font-size:var(--text-small);text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.screen-tip-kind{border:1px solid var(--rule-strong);border-radius:var(--radius-s);color:var(--ink-2);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;flex:none;margin-left:auto;padding:1px 5px;font-weight:600}.screen-tip .screen-tip-class{color:var(--ink-2);font-size:var(--text-meta);overflow-wrap:anywhere;margin-top:4px}.screen-tip .screen-tip-time{justify-content:space-between;gap:var(--space-2);border-top:1px solid var(--rule);color:var(--ink);font-size:var(--text-meta);margin-top:6px;padding-top:6px;display:flex}.screen-tip-time span{color:var(--ink-muted)}@media (forced-colors:active){.screen-band{border:1px solid canvastext}.screen-band[data-selected]{outline:2px solid highlight}}.timeline{flex-direction:column;flex:auto;min-height:0;display:flex;position:relative}.timeline-toolbar{z-index:3;align-items:center;gap:var(--space-2);min-height:40px;padding:5px var(--space-4);border-bottom:1px solid var(--rule);background:var(--surface);flex-wrap:wrap;display:flex;position:relative}.timeline-toolbar>.overline{margin-right:auto}.timeline-lanes{flex-direction:column;flex:auto;min-height:0;display:flex;overflow-y:auto}.timeline-lanes[data-dense] .lane:not(.lane--screens){min-height:108px}.timeline-lanes[data-dense] .lane-plot{min-height:68px}.lane{border-top:1px solid var(--rule);min-height:136px;animation:lane-in .42s var(--ease-out) both;flex-direction:column;flex:1 1 0;margin:0;display:flex}.lane:first-child{border-top:0}.lane:nth-child(2){animation-delay:40ms}.lane:nth-child(3){animation-delay:80ms}.lane:nth-child(4){animation-delay:.12s}.lane:nth-child(n+5){animation-delay:.16s}@keyframes lane-in{0%{opacity:0;transform:translateY(4px)}}.lane-head{justify-content:space-between;align-items:baseline;gap:var(--space-1) var(--space-4);padding:var(--space-2) var(--space-4) 0;flex-wrap:wrap;display:flex}.lane-name{align-items:baseline;gap:var(--space-2);min-width:0;display:flex}.lane-title{font-size:var(--text-small);letter-spacing:.01em;margin:0;font-weight:600}.lane-unit{color:var(--ink-muted);font-size:var(--text-meta);white-space:nowrap}.lane-hint{width:18px;height:18px;color:var(--ink-muted);cursor:help;border-radius:50%;align-self:center;place-items:center;display:inline-grid}.lane-hint:hover,.lane-hint:focus-visible{color:var(--ink);background:var(--hover-wash)}.lane-toggle{border:1px solid var(--rule-strong);border-radius:var(--radius-s);height:20px;color:var(--ink-2);font-family:var(--font-mono);font-size:var(--text-overline);transition:background-color var(--dur-fast) linear, color var(--dur-fast) linear;background:0 0;align-self:center;padding:0 7px;font-weight:500}.lane-toggle:hover{background:var(--hover-wash);color:var(--ink)}.lane-toggle[aria-pressed=true]{border-color:var(--ink);background:var(--ink);color:var(--ink-inverse)}.lane-legend{gap:2px var(--space-3);flex-wrap:wrap;margin:0;padding:0;list-style:none;display:flex}.legend-item{font-size:var(--text-meta);white-space:nowrap;align-items:center;gap:6px;display:inline-flex}.legend-label{color:var(--ink-muted)}.legend-value{border-radius:var(--radius-s);min-width:3.5ch;color:var(--ink);font-size:var(--text-meta);text-align:right;transition:background-color var(--dur-fast) linear;padding:0 3px;font-weight:500}.lane-legend[data-at-cursor] .legend-value{background:var(--hover-wash)}.legend-key{background:var(--key,var(--ink));border-radius:1px;flex:none;width:14px;height:2px;display:inline-block}.legend-key[data-key=thin]{height:1px}.legend-key[data-key=area]{border-radius:2px;width:9px;height:9px}.legend-key[data-key=bar]{border-radius:2px 2px 0 0;width:4px;height:10px}.legend-key[data-tint]{background:color-mix(in srgb, var(--key,var(--ink)) 45%, var(--surface))}.legend-key[data-color=cpu]{--key:var(--viz-cpu)}.legend-key[data-color=fps]{--key:var(--viz-fps)}.legend-key[data-color=jank]{--key:var(--jank)}.legend-key[data-color=frameTime]{--key:var(--viz-frametime)}.legend-key[data-color=frameMax]{--key:var(--viz-framemax)}.legend-key[data-color=memNative]{--key:var(--viz-mem-native)}.legend-key[data-color=memJava]{--key:var(--viz-mem-java)}.legend-key[data-color=memGraphics]{--key:var(--viz-mem-graphics)}.legend-key[data-color=memCode]{--key:var(--viz-mem-code)}.legend-key[data-color=memOther]{--key:var(--viz-mem-other)}.legend-key[data-color=memTotal]{--key:var(--viz-mem-total)}.legend-key[data-color=ink]{--key:var(--ink)}[data-color=stageInput]{--key:var(--viz-stage-input)}[data-color=stageAnimation]{--key:var(--viz-stage-animation)}[data-color=stageLayout]{--key:var(--viz-stage-layout)}[data-color=stageDraw]{--key:var(--viz-stage-draw)}[data-color=stageSync]{--key:var(--viz-stage-sync)}[data-color=stageCommand]{--key:var(--viz-stage-command)}[data-color=stageSwap]{--key:var(--viz-stage-swap)}[data-color=stageDelay]{--key:var(--viz-stage-delay)}[data-color=stageGpu]{--key:var(--viz-stage-gpu)}.legend-item[data-emphasis] .legend-label{color:var(--ink);font-weight:600}.legend-item[data-emphasis] .legend-value{box-shadow:inset 0 0 0 1px var(--rule-strong);font-weight:600}.legend-item[title]{cursor:help}.source-tag{border:1px solid var(--rule-strong);border-radius:var(--radius-s);color:var(--ink-2);font-family:var(--font-mono);font-size:var(--text-overline);white-space:nowrap;align-self:center;padding:1px 6px;font-weight:500}.source-tag[data-source=sdk]{border-color:color-mix(in srgb, var(--good) 45%, var(--rule-strong));background:var(--good-wash);color:var(--ink)}.source-tag[data-idle]{color:var(--ink-2);cursor:help;background:0 0;border-style:dashed}.lane-plot{flex:auto;min-height:92px;position:relative;overflow:hidden}.lane-plot>.uplot{width:100%;font-family:var(--font-mono);position:absolute;inset:0}.lane-plot .u-hz .u-cursor-x{border-right:1px solid var(--ink-muted)}.lane-plot .u-cursor-pt{box-shadow:0 0 0 1px var(--rule-strong)}.lane-probe{border-left:1px solid var(--ink);opacity:0;pointer-events:none;width:0;transition:opacity var(--dur-fast) linear;position:absolute;top:0;bottom:0;left:0}.lane-probe:before{background:var(--ink);width:7px;height:7px;box-shadow:0 0 0 2px var(--surface);content:"";border-radius:50%;position:absolute;top:0;left:-4px}.timeline-notice{align-items:center;gap:var(--space-2);padding:6px var(--space-4);border-bottom:1px solid var(--rule);background:var(--surface-sunk);color:var(--ink-2);font-size:var(--text-small);display:flex}.timeline-overlay{z-index:2;padding:var(--space-5);animation:overlay-in var(--dur) var(--ease-out) both;place-items:center;display:grid;position:absolute;inset:0}@keyframes overlay-in{0%{opacity:0}}@media (width<=1024px){.lane{flex:none;height:210px}}@media (width<=640px){.lane{height:184px}.lane-head{padding-inline:var(--space-3)}}.topbar{z-index:10;align-items:flex-end;gap:var(--space-5);padding:10px var(--space-4) 10px;border-bottom:1px solid var(--rule-strong);background:var(--plane);display:flex;position:relative}.topbar-brand{flex-direction:column;flex:none;align-self:center;gap:1px;display:flex}.brand{color:var(--ink);align-items:center;gap:8px;display:inline-flex}.brand-mark{flex:none}.brand-dot{fill:var(--brand)}.brand-word{font-size:var(--text-title);letter-spacing:-.015em;font-weight:600;line-height:1}.brand-word span{color:var(--ink-2);font-weight:400}.topbar-version{color:var(--ink-muted);font-size:var(--text-overline);letter-spacing:.02em;padding-left:30px}.topbar-controls{align-items:flex-end;gap:var(--space-2) var(--space-3);min-width:0;padding-left:var(--space-5);border-left:1px solid var(--rule-strong);flex-wrap:wrap;flex:auto;display:flex}.field{gap:3px;min-width:0;display:grid}.field--grow{flex:260px;max-width:440px}.field-row{gap:var(--space-1);display:flex}.select-wrap,.combo{min-width:0;position:relative}.select-wrap{flex:auto}.select,.combo-input{border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface);width:100%;height:30px;color:var(--ink);font-size:var(--text-small);text-overflow:ellipsis;transition:border-color var(--dur-fast) linear;padding:0 28px 0 10px}.select{appearance:none;cursor:pointer;width:300px;max-width:100%}.select:hover:not(:disabled),.combo-input:hover:not(:disabled){border-color:var(--ink-muted)}.select:disabled,.combo-input:disabled{background:var(--surface-sunk);color:var(--ink-2);cursor:not-allowed}.combo-input::placeholder{color:var(--ink-muted);font-family:var(--font-sans)}.combo[data-open] .combo-input{border-color:var(--focus);outline:none}.select-chevron{color:var(--ink-muted);pointer-events:none;transition:transform var(--dur) var(--ease-out);position:absolute;top:50%;right:10px;transform:translateY(-50%)}.combo[data-open] .select-chevron{transform:translateY(-50%)rotate(180deg)}.combo-pop{z-index:30;border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-raised);width:max(100%,380px);max-width:calc(100vw - 32px);max-height:min(420px,60vh);box-shadow:var(--shadow-pop);transform-origin:0 0;animation:pop-in .16s var(--ease-out) both;flex-direction:column;display:flex;position:absolute;top:calc(100% + 4px);left:0}@keyframes pop-in{0%{opacity:0;transform:translateY(-3px)scale(.99)}}.combo-toolbar{justify-content:space-between;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--rule);display:flex}.check{font-size:var(--text-small);cursor:pointer;align-items:center;gap:6px;display:inline-flex}.check input{width:14px;height:14px;accent-color:var(--ink);margin:0}.combo-status{color:var(--ink-muted);font-size:var(--text-meta);white-space:nowrap}.combo-list{padding:var(--space-1);flex:auto;margin:0;list-style:none;overflow-y:auto}.combo-option{align-items:center;gap:var(--space-2);min-height:30px;padding:0 var(--space-2);border-radius:var(--radius-s);cursor:pointer;display:flex}.combo-option[aria-selected=true]{background:var(--active-wash)}.combo-option[data-current]{box-shadow:inset 2px 0 0 var(--ink)}.combo-option[data-kind=reveal]{color:var(--ink-2);font-size:var(--text-small)}.combo-option[data-kind=reveal] .combo-name:before{color:var(--ink-muted);content:"+";margin-right:6px}.combo-note{padding:6px var(--space-3);border-bottom:1px solid var(--rule);background:var(--surface-sunk);color:var(--ink-2);font-size:var(--text-meta);margin:0}.combo-note--foot{border-top:1px solid var(--rule);border-bottom:0}.combo-kbd{border:1px solid var(--rule-strong);border-radius:var(--radius-s);color:var(--ink-muted);font-size:var(--text-overline);padding:0 4px}.combo-name{min-width:0;font-size:var(--text-small);text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.combo-name mark{background:var(--event-wash);color:inherit;border-radius:2px}.combo-empty{padding:var(--space-3) var(--space-2);color:var(--ink-muted);font-size:var(--text-small)}.pid-badge{border-radius:var(--radius-s);background:var(--good-wash);color:var(--good);font-size:var(--text-overline);flex:none;padding:1px 6px;font-weight:600}.sys-badge{color:var(--ink-muted);font-size:var(--text-overline);letter-spacing:.04em;text-transform:uppercase;flex:none}.topbar-session{align-items:center;gap:var(--space-3);min-width:0;display:flex}.session-btn{min-width:84px}.session-btn--stop{border-color:var(--ink);font-weight:600}.status-pill{border:1px solid var(--rule-strong);border-radius:var(--radius-pill);background:var(--surface);min-width:0;max-width:360px;height:26px;color:var(--ink-2);font-size:var(--text-small);white-space:nowrap;align-items:center;gap:6px;padding:0 10px;font-weight:500;display:inline-flex}.status-dot{background:var(--ink-muted);border-radius:50%;flex:none;width:7px;height:7px;position:relative}.status-meta{color:var(--ink-muted);font-size:var(--text-meta);font-weight:400}.status-meta:before{color:var(--rule-strong);content:"·";margin-right:6px}.status-error{font-family:var(--font-sans);text-overflow:ellipsis;overflow:hidden}.status-pill[data-status=running]{border-color:color-mix(in srgb, var(--good) 45%, var(--rule-strong));color:var(--ink)}.status-pill[data-status=running] .status-dot{background:var(--good)}.status-pill[data-status=running] .status-dot:after,.status-pill[data-status=waiting] .status-dot:after{content:"";animation:pulse 1.6s var(--ease-out) infinite;border:1.5px solid;border-radius:50%;position:absolute;inset:-3px}.status-pill[data-status=running] .status-dot:after{color:var(--good)}.status-pill[data-status=waiting]{color:var(--ink)}.status-pill[data-status=waiting] .status-dot{background:var(--ink-2)}.status-pill[data-status=waiting] .status-dot:after{color:var(--ink-2)}.status-pill[data-status=error]{border-color:color-mix(in srgb, var(--critical) 50%, var(--rule-strong));background:var(--critical-wash);color:var(--log-e)}@keyframes pulse{0%{opacity:.9;transform:scale(.6)}to{opacity:0;transform:scale(1.6)}}@media (prefers-reduced-motion:reduce){.status-dot:after{display:none}}.sdk-status{position:relative}.sdk-pill{border:1px solid var(--rule-strong);border-radius:var(--radius-pill);background:var(--surface);height:26px;color:var(--ink-2);font-size:var(--text-small);white-space:nowrap;align-items:center;gap:6px;padding:0 10px;font-weight:500;display:inline-flex}.sdk-dot{background:var(--ink-muted);border-radius:2px;flex:none;width:7px;height:7px}.sdk-pill[data-state=connected]{color:var(--ink)}.sdk-pill[data-state=connected] .sdk-dot{background:var(--good)}.sdk-meta{color:var(--ink-muted);font-size:var(--text-meta);font-weight:400}.sdk-meta:before{color:var(--rule-strong);content:"·";margin:0 5px}.sdk-pill[data-state=disconnected] .sdk-dot{box-shadow:inset 0 0 0 1.5px var(--ink-muted);background:0 0}button.sdk-pill{transition:background-color var(--dur-fast) linear, border-color var(--dur-fast) linear;border-style:dashed}button.sdk-pill:hover,button.sdk-pill[aria-expanded=true]{border-color:var(--ink-muted);background:var(--hover-wash);color:var(--ink)}button.sdk-pill .sdk-dot{box-shadow:inset 0 0 0 1.5px var(--ink-muted);background:0 0}.sdk-pop{z-index:30;gap:var(--space-2);width:540px;max-width:calc(100vw - 32px);padding:var(--space-3);border:1px solid var(--rule-strong);border-radius:var(--radius-m);background:var(--surface-raised);box-shadow:var(--shadow-pop);transform-origin:100% 0;animation:pop-in .16s var(--ease-out) both;grid-template-columns:minmax(0,1fr);display:grid;position:absolute;top:calc(100% + 6px);right:0}@media (width<=640px){.sdk-pop{transform-origin:0 0;left:0;right:auto}}button.sdk-pill[data-state=failed]{border-style:solid;border-color:color-mix(in srgb, var(--critical) 50%, var(--rule-strong));background:var(--critical-wash);color:var(--log-e)}button.sdk-pill[data-state=failed] .sdk-dot{background:var(--critical);box-shadow:none}.sdk-pop:focus-visible{outline-offset:0}.sdk-pop-error{padding:var(--space-2) var(--space-3);border-left:2px solid var(--critical);border-radius:var(--radius-s);background:var(--critical-wash);color:var(--ink);font-size:var(--text-meta);overflow-wrap:anywhere;margin:0}.sdk-pop-head{justify-content:space-between;align-items:center;gap:var(--space-2);display:flex}.sdk-pop-title{font-size:var(--text-small);margin:0;font-weight:600}.sdk-pop-close{border-color:#0000;width:24px;height:24px}.sdk-pop-text{color:var(--ink-2);font-size:var(--text-meta);margin:0;line-height:1.5}.sdk-snippet{border:1px solid var(--rule);border-radius:var(--radius-s);background:var(--surface-sunk);position:relative}.sdk-snippet pre{padding:var(--space-2) var(--space-3);color:var(--ink);font-size:var(--text-meta);margin:0;line-height:1.55;overflow-x:auto}.sdk-snippet-bar{justify-content:space-between;align-items:center;gap:var(--space-2);padding:4px 4px 4px var(--space-3);border-bottom:1px solid var(--rule);display:flex}.sdk-copy{height:24px;padding:0 var(--space-2);font-size:var(--text-meta)}.topbar-link{color:var(--ink-muted);font-size:var(--text-meta);flex:none;align-self:center;align-items:center;gap:6px;margin-left:auto;display:inline-flex}.link-dot{background:var(--good);border-radius:50%;width:6px;height:6px}.topbar-link:not([data-state=open]) .link-dot{background:var(--critical)}@media (width<=1180px){.topbar{align-items:center;gap:var(--space-2) var(--space-4);flex-wrap:wrap}.topbar-controls{border-left:0;flex-basis:100%;order:3;padding-left:0}}@media (width<=640px){.field,.field--grow{flex:100%;max-width:none}.select{width:100%}.topbar-session{flex-wrap:wrap;flex:100%}.status-pill{max-width:100%}}.app{flex-direction:column;min-width:0;height:100dvh;display:flex}.banners:empty{display:none}.workspace{background:var(--rule-strong);flex:auto;grid-template-columns:minmax(0,2fr) minmax(360px,1fr);gap:1px;min-height:0;display:grid}.instrument{background:var(--surface);flex-direction:column;min-width:0;min-height:0;display:flex}.timeline-notice{margin:0}@media (width<=1024px){.app{height:auto;min-height:100dvh}.workspace{grid-template-columns:minmax(0,1fr)}.side{height:min(72vh,680px)}}
|