Allow debugger to load a trace from Local Storage.
This gives shaders.skia.org a simple pathway to hand off a trace to the
debugger. When a trace is collected, we can store it in Local Storage
and then redirect the user to `/debug?local-storage`, which will detect
a usable DebugTrace JSON in Local Storage and then immediately debug it
instead of prompting the user for a file.
The query parameter exists to avoid accidentally locking users into a
trace forever. Traces can always be loaded by dragging them in, but
without the initial prompt, a user won't know to try it.
Testing is accomplished by giving the test code hooks which can replace
the window's query parameter and Local Storage object at
initialization time.
Bug: skia:12818
Change-Id: I8f4a61a9efd8f523a2e517d40f6e5b42d3753fae
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/494823
Auto-Submit: John Stiles <johnstiles@google.com>
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/shaders/modules/debugger-app-sk/debugger-app-sk.ts b/shaders/modules/debugger-app-sk/debugger-app-sk.ts
index 7282ca8..31586bb 100644
--- a/shaders/modules/debugger-app-sk/debugger-app-sk.ts
+++ b/shaders/modules/debugger-app-sk/debugger-app-sk.ts
@@ -35,6 +35,11 @@
modeProps: { fold: ['brace', 'include'] },
});
+enum ErrorReporting {
+ Yes = 1,
+ No = 0
+}
+
export class DebuggerAppSk extends ElementSk {
private trace: DebugTrace | null = null;
@@ -44,10 +49,22 @@
private currentLineHandle: CodeMirror.LineHandle | null = null;
+ private localStorage: Storage = window.localStorage; // can be overridden in tests
+
+ private queryParameter: string = window.location.search; // can be overridden in tests
+
constructor() {
super(DebuggerAppSk.template);
}
+ setLocalStorageForTest(mockStorage: Storage): void {
+ this.localStorage = mockStorage;
+ }
+
+ setQueryParameterForTest(overrideQueryParam: string): void {
+ this.queryParameter = overrideQueryParam;
+ }
+
private static themeFromCurrentMode(): string {
return isDarkMode() ? 'ambiance' : 'base16-light';
}
@@ -80,6 +97,18 @@
document.addEventListener('theme-chooser-toggle', () => {
this.codeMirror!.setOption('theme', DebuggerAppSk.themeFromCurrentMode());
});
+
+ // If ?local-storage(=anything), try loading a debug trace from local storage.
+ const params = new URLSearchParams(this.queryParameter);
+ if (params.has('local-storage')) {
+ this.loadJSONData(this.localStorage.getItem('sksl-debug-trace')!, ErrorReporting.No);
+
+ // Remove ?local-storage from the query parameters on the window, so a reload or copy-paste
+ // will present a clean slate.
+ const url = new URL(window.location.toString());
+ url.searchParams.delete('local-storage');
+ window.history.pushState({}, '', url.toString());
+ }
}
getEditor(): CodeMirror.Editor | null {
@@ -135,7 +164,7 @@
return [html`<tr><td> </td></tr>`];
}
- loadJSONData(jsonData: string): void {
+ loadJSONData(jsonData: string, reportErrors?: ErrorReporting): void {
try {
this.trace = Convert.toDebugTrace(jsonData);
this.codeMirror!.setValue(this.trace.source.join('\n'));
@@ -144,7 +173,9 @@
this.resetBreakpointGutter();
this._render();
} catch (ex) {
- this.codeMirror!.setValue((ex instanceof Error) ? ex.message : String(ex));
+ if (reportErrors ?? ErrorReporting.Yes) {
+ this.codeMirror!.setValue((ex instanceof Error) ? ex.message : String(ex));
+ }
}
}
diff --git a/shaders/modules/debugger-app-sk/debugger-app-sk_test.ts b/shaders/modules/debugger-app-sk/debugger-app-sk_test.ts
index 6f939c0..92c0f40 100644
--- a/shaders/modules/debugger-app-sk/debugger-app-sk_test.ts
+++ b/shaders/modules/debugger-app-sk/debugger-app-sk_test.ts
@@ -6,6 +6,30 @@
import { exampleTraceString } from './demo_data';
import CodeMirror from 'codemirror';
+function makeFakeLocalStorage(store: Record<string, string>): Storage {
+ const fakeLocalStorage: Storage = {
+ length: 0,
+ getItem: (key: string): string | null => {
+ return key in store ? store[key] as string : null;
+ },
+ setItem: (key: string, value: string) => {
+ store[key] = `${value}`;
+ length = Object.keys(store).length;
+ },
+ removeItem: (key: string) => {
+ delete store[key];
+ length = Object.keys(store).length;
+ },
+ clear: () => {
+ store = {};
+ },
+ key: function (index: number): string | null {
+ return Object.keys(store)[index];
+ }
+ };
+ return fakeLocalStorage;
+}
+
function getLinesWithBgClass(app: DebuggerAppSk, expectedType: string): number[] {
const editor: CodeMirror.Editor = app.getEditor()!;
assert.isNotNull(editor);
@@ -58,9 +82,9 @@
return getLinesWithBreakpointMarker(app, 'cm-breakpoint');
}
-describe('debugger-app-sk', () => {
- const newInstance = setUpElementUnderTest<DebuggerAppSk>('debugger-app-sk');
+const newInstance = setUpElementUnderTest<DebuggerAppSk>('debugger-app-sk');
+describe('debugger app', () => {
let debuggerAppSk: DebuggerAppSk;
beforeEach(() => {
@@ -161,3 +185,51 @@
assert.equal(getCurrentLine(debuggerAppSk), null);
});
});
+
+describe('local storage', () => {
+ let debuggerAppSk: DebuggerAppSk;
+
+ it('loads a trace when populated and ?local-storage query param exists', () => {
+ debuggerAppSk = newInstance((self: DebuggerAppSk) => {
+ self.setLocalStorageForTest(makeFakeLocalStorage({'sksl-debug-trace': exampleTraceString}));
+ self.setQueryParameterForTest('?local-storage');
+ });
+
+ const codeAreaText = $$<HTMLDivElement>('#codeEditor')?.innerText;
+ assert.include(codeAreaText, 'half4 convert(float2 c) {');
+ assert.include(codeAreaText, 'half4 c = convert(p * 0.001);');
+ assert.notInclude(codeAreaText, 'Drag in a DebugTrace JSON file to start the debugger.');
+ });
+
+ it('does nothing when ?local-storage query param is not present', () => {
+ debuggerAppSk = newInstance((self: DebuggerAppSk) => {
+ self.setLocalStorageForTest(makeFakeLocalStorage({'sksl-debug-trace': exampleTraceString}));
+ });
+
+ const codeAreaText = $$<HTMLDivElement>('#codeEditor')?.innerText;
+ assert.notInclude(codeAreaText, 'half4 convert(float2 c) {');
+ assert.include(codeAreaText, 'Drag in a DebugTrace JSON file to start the debugger.');
+ });
+
+ it('does nothing when local storage is invalid, even if ?local-storage is set', () => {
+ debuggerAppSk = newInstance((self: DebuggerAppSk) => {
+ self.setLocalStorageForTest(makeFakeLocalStorage({'sksl-debug-trace': '{}'}));
+ self.setQueryParameterForTest('?local-storage');
+ });
+
+ const codeAreaText = $$<HTMLDivElement>('#codeEditor')?.innerText;
+ assert.notInclude(codeAreaText, 'half4 convert(float2 c) {');
+ assert.include(codeAreaText, 'Drag in a DebugTrace JSON file to start the debugger.');
+ });
+
+ it('does nothing when local storage is empty, even if ?local-storage is set', () => {
+ debuggerAppSk = newInstance((self: DebuggerAppSk) => {
+ self.setLocalStorageForTest(makeFakeLocalStorage({}));
+ self.setQueryParameterForTest('?local-storage');
+ });
+
+ const codeAreaText = $$<HTMLDivElement>('#codeEditor')?.innerText;
+ assert.notInclude(codeAreaText, 'half4 convert(float2 c) {');
+ assert.include(codeAreaText, 'Drag in a DebugTrace JSON file to start the debugger.');
+ });
+});