Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/app/studio/src/errors/ErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import { showErrorDialog, showInfoDialog } from "@/ui/components/dialogs.tsx";
* facing dialog.
*/
export class ErrorHandler {
/** Collects listeners that need to be disposed when the handler stops. */
readonly terminator = new Terminator();
readonly #service: StudioService;

/** Guard to avoid infinite error loops once an error has been processed. */
#errorThrown: boolean = false;

constructor(service: StudioService) {
Expand Down Expand Up @@ -87,6 +89,7 @@ export class ErrorHandler {
*
* @param owner - Window or worker to attach listeners to.
* @param scope - Descriptive name of the owner for reporting.
* @returns Terminable that removes the listeners.
*/
install(
owner: WindowProxy | Worker | AudioWorkletNode,
Expand Down
4 changes: 4 additions & 0 deletions packages/app/studio/src/errors/ErrorInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { isDefined } from "@opendaw/lib-std";

/** Normalized representation of diverse error events. */
export type ErrorInfo = {
/** Name of the error type. */
name: string;
/** Optional human-readable error message. */
message?: string;
/** Raw stack trace if available. */
stack?: string;
};

Expand All @@ -28,6 +31,7 @@ export namespace ErrorInfo {
if (reason instanceof Error) {
if (!isDefined(reason.stack)) {
try {
// Re-throwing captures a stack when one isn't present.
// noinspection ExceptionCaughtLocallyJS
throw reason;
} catch (error) {
Expand Down
7 changes: 6 additions & 1 deletion packages/app/studio/src/errors/ErrorLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export type ErrorLog = {
agent: string;
/** Build information for the running application. */
build: BuildInfo;
/** Number of script tags present on the page. */
/**
* Number of `<script>` elements on the current page.
*
* A higher count than expected can hint at browser extensions
* injecting code and causing interference.
*/
scripts: int;
/** Normalized error information. */
error: ErrorInfo;
Expand Down
6 changes: 6 additions & 0 deletions packages/app/studio/src/errors/LogBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ import { int } from "@opendaw/lib-std";
export namespace LogBuffer {
/** Single console log entry preserved for debugging. */
export type Entry = {
/** Timestamp in milliseconds since epoch when the log was recorded. */
time: number;
/** Console severity level for the entry. */
level: "debug" | "info" | "warn";
/** Arguments forwarded to the original console method. */
args: Array<string>;
};

const logBuffer: Entry[] = [];

if (import.meta.env.PROD) {
// Upper bound for the number of argument strings stored to avoid
// unbounded memory usage in long running sessions.
let estimatedSize: int = 0;
const MAX_ARGS_SIZE = 100_000;
const pushLog = (level: Entry["level"], args: unknown[]) => {
Expand Down Expand Up @@ -69,6 +74,7 @@ export namespace LogBuffer {
// Last resort fallback
return Object.prototype.toString.call(value);
} catch {
// Catch and mark values that throw during conversion.
return "[unserializable]";
}
};
Expand Down
6 changes: 6 additions & 0 deletions packages/app/studio/src/service/SyncLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export namespace SyncLogService {
const label: FooterLabel = asDefined(service.factoryFooterLabel().unwrap()())
label.setTitle("SyncLog")
let count = 0 | 0
// Attach a writer that updates the footer after each commit is flushed.
SyncLogWriter.attach(service.project, wrapBlockWriter(handle, () => label.setValue(`${++count} commits`)))
}

Expand Down Expand Up @@ -69,6 +70,7 @@ export namespace SyncLogService {
console.warn("arrayBuffer", arrayBufferResult.error)
return
}
// Deserialize the existing log to restore the project and seek the last commit.
const {project, lastCommit, numCommits} = await SyncLogReader.unwrap(service, arrayBufferResult.value)
service.fromProject(project, "SyncLog")
const label: FooterLabel = asDefined(service.factoryFooterLabel().unwrap()())
Expand All @@ -85,6 +87,8 @@ export namespace SyncLogService {
* @returns Observer function passed to {@link SyncLogWriter.attach}.
*/
const wrapBlockWriter = (handle: FileSystemFileHandle, callback: Exec) => {
// Queue of commits waiting to be written to disk. The promise chain
// ensures commits are flushed sequentially in order.
let blocks: Array<Commit> = []
let lastPromise: Promise<void> = Promise.resolve()
return (commit: Commit): void => {
Expand All @@ -93,6 +97,7 @@ export namespace SyncLogService {
lastPromise = lastPromise.then(async () => {
const writable: FileSystemWritableFileStream = await handle.createWritable({keepExistingData: true})
const file = await handle.getFile()
// Append to the end of the existing file without truncating it.
await writable.seek(file.size)
const buffers = blocks.map(block => block.serialize())
blocks = []
Expand All @@ -111,6 +116,7 @@ export namespace SyncLogService {
const appendArrayBuffers = (buffers: ReadonlyArray<ArrayBuffer>): ArrayBuffer => {
const totalLength = buffers.reduce((sum, buffer) => sum + buffer.byteLength, 0)
const result = new Uint8Array(totalLength)
// Copy buffers one after another into the result view.
buffers.reduce((offset, buffer) => {
result.set(new Uint8Array(buffer), offset)
return offset + buffer.byteLength
Expand Down
30 changes: 30 additions & 0 deletions packages/docs/docs-dev/debugging/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Logging

openDAW provides several lightweight helpers for collecting diagnostic
information while developing or running the Studio.

## Console capture

The [`LogBuffer`](/dev/debugging/error-logging#logbuffer) monkey patches the
browser's console methods in production builds and keeps a bounded history of
recent messages. When an error is reported the buffer is included in the
payload so that the server receives context for the failure.

## Sync logs

Project changes can be serialized using the `SyncLogWriter` and related
utilities. The `SyncLogService` integrates these pieces into the Studio and can
start a new log or append to an existing file. This stream of commits is useful
for reproducing complex editing sessions.

## Timing helpers

For ad-hoc instrumentation the `stopwatch` function prints labelled laps to the
console, while `TimeSpanUtils.startEstimator` returns a function that projects
the remaining time of a process based on its progress.

## Warnings

The `warn` helper throws a dedicated `Warning` error type. The global
`ErrorHandler` treats these differently from fatal exceptions and displays an
informational dialog to the user.
1 change: 1 addition & 0 deletions packages/docs/docs-dev/debugging/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
openDAW provides a set of utilities to help developers diagnose problems during development and in production.

- [Error logging](error-logging.md) describes how runtime issues are captured and reported.
- [Logging helpers](logging.md) covers general purpose utilities such as `LogBuffer` and `stopwatch`.
- [Graph runtime](graph-runtime.md) explains the interactive graph view used to inspect box connections.
6 changes: 6 additions & 0 deletions packages/docs/docs-user/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
- Developers looking to diagnose issues in depth can consult the
[debugging guide](/dev/debugging/overview).

### How do I collect logs for support?

- The Studio automatically captures recent console output and sends it along
with error reports. You can also export a project `SyncLog` from the footer
menu to share a history of your edits.

### App stuck on loading screen

- Check your internet connection and reload the page.
Expand Down
1 change: 1 addition & 0 deletions packages/lib/runtime/src/stopwatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface Stopwatch {
* @returns A stopwatch instance.
*/
export const stopwatch = (level: "debug" | "info" = "debug"): Stopwatch => {
// Capture the start time once; each lap uses the delta to this point.
const startTime = performance.now()
return {
lab: (label: string) =>
Expand Down
1 change: 1 addition & 0 deletions packages/lib/runtime/src/timespan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export namespace TimeSpanUtils {
export const startEstimator = (): Func<number, TimeSpan> => {
const startTime: number = performance.now()
return (progress: unitValue): TimeSpan => {
// Avoid division by zero when no progress has been reported yet.
if (progress === 0.0) {return TimeSpan.POSITIVE_INFINITY}
const runtime = performance.now() - startTime
return TimeSpan.millis(runtime / progress - runtime)
Expand Down
6 changes: 6 additions & 0 deletions packages/lib/std/src/warning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
*/
export class Warning extends Error {}

/**
* Throws a {@link Warning} with the given message.
*
* This utility allows signalling non-fatal issues that should surface to the
* user but are handled differently from regular errors.
*/
export const warn = (issue: string): never => {
throw new Warning(issue);
};
13 changes: 12 additions & 1 deletion packages/studio/core/src/sync-log/SyncLogWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ export class SyncLogWriter implements Terminable {
readonly #observer: Observer<Commit>
readonly #subscription: Subscription

/**
* Subscription that stays active during a box-graph transaction to collect
* updates. Reset after each commit.
*/
#transactionSubscription: Subscription = Terminable.Empty
/** Promise chain used to serialize commit writing. */
#lastPromise: Promise<Commit>

private constructor(project: Project, observer: Observer<Commit>, lastCommit?: Commit) {
Expand All @@ -42,7 +47,12 @@ export class SyncLogWriter implements Terminable {
this.#subscription.terminate()
}

/** Queue a commit factory to run after the previous commit has been written. */
/**
* Queue a commit factory to run after the previous commit has finished.
*
* This ensures that commits are written in order even when asynchronous
* operations are involved.
*/
#appendCommit(factory: Func<Commit, Promise<Commit>>): Promise<Commit> {
return this.#lastPromise = this.#lastPromise.then(async (previous) => {
const commit = await factory(previous)
Expand All @@ -53,6 +63,7 @@ export class SyncLogWriter implements Terminable {

/** Listen to box graph transactions and create update commits. */
#listen(boxGraph: BoxGraph): Subscription {
// Collected updates for the current transaction.
let updates: Array<Update> = []
return boxGraph.subscribeTransaction({
onBeginTransaction: () =>
Expand Down