refactor(struct): 10% performance improvement

This commit is contained in:
Simon Chan 2022-05-22 05:23:18 +08:00
parent fa8d251bf2
commit ff708def46
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
9 changed files with 176 additions and 234 deletions

View file

@ -54,6 +54,7 @@ const state = makeAutoObservable({
logcat: undefined as Logcat | undefined, logcat: undefined as Logcat | undefined,
running: false, running: false,
buffer: [] as LogRow[], buffer: [] as LogRow[],
flushRequested: false,
list: [] as LogRow[], list: [] as LogRow[],
count: 0, count: 0,
stream: undefined as ReadableStream<LogMessage> | undefined, stream: undefined as ReadableStream<LogMessage> | undefined,
@ -73,21 +74,20 @@ const state = makeAutoObservable({
new WritableStream({ new WritableStream({
write: (chunk) => { write: (chunk) => {
this.buffer.push(chunk); this.buffer.push(chunk);
if (!this.flushRequested) {
this.flushRequested = true;
requestAnimationFrame(this.flush);
}
}, },
}), }),
{ signal: this.stopSignal.signal } { signal: this.stopSignal.signal }
) )
.catch(() => { }); .catch(() => { });
this.flush();
}, },
flush() { flush() {
if (this.buffer.length) {
this.list.push(...this.buffer); this.list.push(...this.buffer);
this.buffer = []; this.buffer = [];
} this.flushRequested = false;
if (this.running) {
requestAnimationFrame(this.flush);
}
}, },
stop() { stop() {
this.running = false; this.running = false;

View file

@ -34,7 +34,8 @@ export const AdbSyncLstatResponse =
get permission() { return this.mode & 0b00001111_11111111; }, get permission() { return this.mode & 0b00001111_11111111; },
}) })
.postDeserialize((object) => { .postDeserialize((object) => {
if (object.mode === 0 && if (
object.mode === 0 &&
object.size === 0 && object.size === 0 &&
object.mtime === 0 object.mtime === 0
) { ) {

View file

@ -26,38 +26,25 @@ export class BufferedStream {
this.reader = stream.getReader(); this.reader = stream.getReader();
} }
/** private async readSource() {
* const { done, value } = await this.reader.read();
* @param length
* @returns
*/
public async read(length: number): Promise<Uint8Array> {
let result: Uint8Array;
let index: number;
if (this.buffered) {
let array = this.buffered;
const offset = this.bufferedOffset;
if (this.bufferedLength > length) {
// PERF: `subarray` is slow
// don't use it until absolutely necessary
this.bufferedOffset += length;
this.bufferedLength -= length;
return array.subarray(offset, offset + length);
}
this.buffered = undefined;
array = array.subarray(offset);
result = new Uint8Array(length);
result.set(array);
index = this.bufferedLength;
length -= this.bufferedLength;
} else {
const { done, value: array } = await this.reader.read();
if (done) { if (done) {
throw new BufferedStreamEndedError(); throw new BufferedStreamEndedError();
} }
return value;
}
private async readAsync(length: number, initial?: Uint8Array) {
let result: Uint8Array;
let index: number;
if (initial) {
result = new Uint8Array(length);
result.set(initial);
index = initial.byteLength;
length -= initial.byteLength;
} else {
const array = await this.readSource();
if (array.byteLength === length) { if (array.byteLength === length) {
return array; return array;
} }
@ -76,11 +63,7 @@ export class BufferedStream {
} }
while (length > 0) { while (length > 0) {
const { done, value: array } = await this.reader.read(); const array = await this.readSource();
if (done) {
throw new BufferedStreamEndedError();
}
if (array.byteLength === length) { if (array.byteLength === length) {
result.set(array, index); result.set(array, index);
return result; return result;
@ -102,6 +85,31 @@ export class BufferedStream {
return result; return result;
} }
/**
*
* @param length
* @returns
*/
public read(length: number): Uint8Array | Promise<Uint8Array> {
// PERF: Add a synchronous path for reading from internal buffer
if (this.buffered) {
const array = this.buffered;
const offset = this.bufferedOffset;
if (this.bufferedLength > length) {
// PERF: `subarray` is slow
// don't use it until absolutely necessary
this.bufferedOffset += length;
this.bufferedLength -= length;
return array.subarray(offset, offset + length);
}
this.buffered = undefined;
return this.readAsync(length, array.subarray(offset));
}
return this.readAsync(length);
}
/** /**
* Return a readable stream with unconsumed data (if any) and * Return a readable stream with unconsumed data (if any) and
* all data from the wrapped stream. * all data from the wrapped stream.

View file

@ -645,13 +645,11 @@ struct.field(
### Relationship between types ### Relationship between types
A `Struct` is a map between keys and `StructFieldDefinition`s. * `StructFieldValue`: Contains value of a field, with optional metadata and accessor methods.
* `StructFieldDefinition`: Definition of a field, can deserialize `StructFieldValue`s from a stream or create them from exist values.
A `StructValue` is a map between keys and `StructFieldValue`s. * `StructValue`: A map between field names and `StructFieldValue`s.
* `Struct`: Definiton of a struct, a map between field names and `StructFieldDefintion`s. May contain extra metadata.
A `Struct` can create (deserialize) multiple `StructValue`s with same field definitions. * Result of `Struct#deserialize()`: A map between field names and results of `StructFieldValue#get()`.
Each time a `Struct` deserialize, each `StructFieldDefinition` in it creates exactly one `StructFieldValue` to be put into the `StructValue`.
### `StructFieldDefinition` ### `StructFieldDefinition`

View file

@ -20,6 +20,11 @@ export abstract class StructFieldValue<
/** Gets the associated `Struct` instance */ /** Gets the associated `Struct` instance */
public readonly struct: StructValue; public readonly struct: StructValue;
public get hasCustomAccessors(): boolean {
return this.get !== StructFieldValue.prototype.get ||
this.set !== StructFieldValue.prototype.set;
}
protected value: TDefinition['TValue']; protected value: TDefinition['TValue'];
public constructor( public constructor(

View file

@ -1,3 +1,5 @@
import type { ValueOrPromise } from "../utils.js";
export interface StructDeserializeStream { export interface StructDeserializeStream {
/** /**
* Read data from the underlying data source. * Read data from the underlying data source.
@ -15,5 +17,5 @@ export interface StructAsyncDeserializeStream {
* The stream must return exactly `length` bytes or data. If that's not possible * The stream must return exactly `length` bytes or data. If that's not possible
* (due to end of file or other error condition), it must throw an error. * (due to end of file or other error condition), it must throw an error.
*/ */
read(length: number): Promise<Uint8Array>; read(length: number): ValueOrPromise<Uint8Array>;
} }

View file

@ -1,4 +1,4 @@
import { StructFieldValue } from "./field-value.js"; import type { StructFieldValue } from "./field-value.js";
export const STRUCT_VALUE_SYMBOL = Symbol("struct-value"); export const STRUCT_VALUE_SYMBOL = Symbol("struct-value");
@ -11,9 +11,15 @@ export class StructValue {
/** /**
* Gets the result struct value object * Gets the result struct value object
*/ */
public readonly value: Record<PropertyKey, unknown> = {}; public readonly value: Record<PropertyKey, unknown>;
public constructor() { public constructor(prototype: any) {
// PERF: `Object.create(extra)` is 50% faster
// than `Object.defineProperties(this.value, extra)`
this.value = Object.create(prototype);
// PERF: `Object.defineProperty` is slow
// but we need it to be non-enumerable
Object.defineProperty( Object.defineProperty(
this.value, this.value,
STRUCT_VALUE_SYMBOL, STRUCT_VALUE_SYMBOL,
@ -31,8 +37,8 @@ export class StructValue {
this.fieldValues[name] = fieldValue; this.fieldValues[name] = fieldValue;
// PERF: `Object.defineProperty` is slow // PERF: `Object.defineProperty` is slow
if (fieldValue.get !== StructFieldValue.prototype.get || // use normal property when possible
fieldValue.set !== StructFieldValue.prototype.set) { if (fieldValue.hasCustomAccessors) {
Object.defineProperty(this.value, name, { Object.defineProperty(this.value, name, {
configurable: true, configurable: true,
enumerable: true, enumerable: true,

View file

@ -490,7 +490,10 @@ export class Struct<
Overwrite<TExtra, T>, Overwrite<TExtra, T>,
TPostDeserialized TPostDeserialized
> { > {
Object.assign(this._extra, Object.getOwnPropertyDescriptors(value)); Object.defineProperties(
this._extra,
Object.getOwnPropertyDescriptors(value)
);
return this as any; return this as any;
} }
@ -540,14 +543,12 @@ export class Struct<
public deserialize( public deserialize(
stream: StructDeserializeStream | StructAsyncDeserializeStream, stream: StructDeserializeStream | StructAsyncDeserializeStream,
): ValueOrPromise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> { ): ValueOrPromise<StructDeserializedResult<TFields, TExtra, TPostDeserialized>> {
const structValue = new StructValue(); const structValue = new StructValue(this._extra);
Object.defineProperties(structValue.value, this._extra);
let promise = SyncPromise.resolve();
return SyncPromise
.try(() => {
let result = SyncPromise.resolve();
for (const [name, definition] of this._fields) { for (const [name, definition] of this._fields) {
result = result promise = promise
.then(() => .then(() =>
definition.deserialize(this.options, stream as any, structValue) definition.deserialize(this.options, stream as any, structValue)
) )
@ -555,8 +556,8 @@ export class Struct<
structValue.set(name, fieldValue); structValue.set(name, fieldValue);
}); });
} }
return result;
}) return promise
.then(() => { .then(() => {
const object = structValue.value; const object = structValue.value;
@ -588,7 +589,7 @@ export class Struct<
} }
} }
} else { } else {
structValue = new StructValue(); structValue = new StructValue({});
for (const [name, definition] of this._fields) { for (const [name, definition] of this._fields) {
const fieldValue = definition.create( const fieldValue = definition.create(
this.options, this.options,

View file

@ -1,185 +1,106 @@
enum State { export interface SyncPromise<T> {
Pending, then<TResult1 = T, TResult2 = never>(
Fulfilled, onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
Rejected, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
): SyncPromise<TResult1 | TResult2>;
valueOrPromise(): T | PromiseLike<T>;
} }
function fulfilledThen<T, TResult1 = T>( interface SyncPromiseStatic {
this: SyncPromise<T>, reject<T = never>(reason?: any): SyncPromise<T>;
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
): SyncPromise<TResult1> { resolve(): SyncPromise<void>;
if (onfulfilled) { resolve<T>(value: T | PromiseLike<T>): SyncPromise<T>;
return SyncPromise.try(() => onfulfilled(this.result as T));
} try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T>;
return this as unknown as SyncPromise<TResult1>;
} }
function rejectedThen<T, TResult1 = T, TResult2 = never>( export const SyncPromise: SyncPromiseStatic = {
this: SyncPromise<T>, reject<T = never>(reason?: any): SyncPromise<T> {
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, return new RejectedSyncPromise(reason);
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null },
): SyncPromise<TResult2> { resolve<T>(value?: T | PromiseLike<T>): SyncPromise<T> {
if (onrejected) { if (
return SyncPromise.try(() => onrejected(this.result)); typeof value === 'object' &&
} value !== null &&
return this as unknown as SyncPromise<TResult2>; typeof (value as PromiseLike<T>).then === 'function'
}
function fulfilledValue<T>(
this: SyncPromise<T>,
) { ) {
return this.result as T; return new PendingSyncPromise(value as PromiseLike<T>);
} else {
return new ResolvedSyncPromise(value as T);
} }
},
function rejectedValue<T>( try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T> {
this: SyncPromise<T>,
): never {
throw this.result;
}
const INTERNAL_CREATED = Symbol('internal-created') as any;
export class SyncPromise<T> implements PromiseLike<T> {
public static reject<T = never>(reason?: any): SyncPromise<T> {
const promise = new SyncPromise<T>(INTERNAL_CREATED);
promise.handleReject(reason);
return promise;
}
public static resolve(): SyncPromise<void>;
public static resolve<T>(value: T | PromiseLike<T>): SyncPromise<T>;
public static resolve<T>(value?: T | PromiseLike<T>): SyncPromise<T> {
if (value instanceof SyncPromise) {
return value;
}
const promise = new SyncPromise<T>(INTERNAL_CREATED);
promise.handleResolve(value!);
return promise;
}
public static try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T> {
try { try {
return SyncPromise.resolve(executor()); return SyncPromise.resolve(executor());
} catch (e) { } catch (e) {
return SyncPromise.reject(e); return SyncPromise.reject(e);
} }
} }
public state: State = State.Pending;
public result: unknown;
public promise: PromiseLike<T> | undefined;
public constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
if (executor === INTERNAL_CREATED) {
return;
}
let promiseResolve: (value: T | PromiseLike<T>) => void;
let promiseReject: (reason?: any) => void;
let settled = false;
let sync = true;
const handleReject = (reason?: any) => {
if (settled) { return; }
settled = true;
if (!sync) {
promiseReject(reason);
return;
}
this.handleReject(reason);
}; };
try { class PendingSyncPromise<T> implements SyncPromise<T> {
executor( private promise: PromiseLike<T>;
(value: T | PromiseLike<T>) => {
if (settled) { return; }
settled = true;
if (!sync) { public constructor(promise: PromiseLike<T>) {
promiseResolve(value); this.promise = promise;
return;
} }
this.handleResolve(value);
},
handleReject
);
} catch (e) {
handleReject(e);
}
if (this.state === State.Pending && !this.promise) {
this.promise = new Promise<T>((resolve, reject) => {
promiseResolve = resolve;
promiseReject = reject;
});
}
sync = false;
}
private handleResolveValue(value: T) {
this.state = State.Fulfilled;
this.result = value;
this.then = fulfilledThen;
this.valueOrPromise = fulfilledValue;
};
private handleResolve(value: T | PromiseLike<T>) {
if (typeof value === 'object' &&
value !== null &&
'then' in value &&
typeof value.then === 'function'
) {
if (value instanceof SyncPromise) {
switch (value.state) {
case State.Fulfilled:
this.handleResolveValue(value.result as T);
return;
case State.Rejected:
this.handleReject(value.result);
return;
}
}
this.promise = value as PromiseLike<T>;
} else {
this.handleResolveValue(value as T);
}
}
private handleReject(reason?: any) {
this.state = State.Rejected;
this.result = reason;
this.then = rejectedThen;
this.valueOrPromise = rejectedValue;
};
public then<TResult1 = T, TResult2 = never>( public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined, onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
): SyncPromise<TResult1 | TResult2> { ) {
// The result promise isn't really a `SyncPromise`, return new PendingSyncPromise<TResult1 | TResult2>(
// but we attach a `valueOrPromise` to it, this.promise.then(onfulfilled, onrejected)
// so it's compatible with `SyncPromise`. );
// PERF: it's 230% faster than creating a real `SyncPromise` instance.
const promise =
this.promise!.then(onfulfilled, onrejected) as unknown as SyncPromise<TResult1 | TResult2>;
promise.valueOrPromise = this.valueOrPromise as unknown as () => TResult1 | TResult2 | PromiseLike<TResult1 | TResult2>;
return promise;
}
public catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
): SyncPromise<T | TResult> {
return this.then(undefined, onrejected);
} }
public valueOrPromise(): T | PromiseLike<T> { public valueOrPromise(): T | PromiseLike<T> {
return this; return this.promise;
}
}
class ResolvedSyncPromise<T> implements SyncPromise<T> {
private value: T;
public constructor(value: T) {
this.value = value;
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
) {
if (!onfulfilled) {
return this as any;
}
return SyncPromise.try(() => onfulfilled(this.value));
}
public valueOrPromise(): T | PromiseLike<T> {
return this.value;
}
}
class RejectedSyncPromise<T> implements SyncPromise<T> {
private reason: any;
public constructor(reason: any) {
this.reason = reason;
}
public then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
) {
if (!onrejected) {
return this as any;
}
return SyncPromise.try(() => onrejected(this.reason));
}
public valueOrPromise(): T | PromiseLike<T> {
throw this.reason;
} }
} }