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

View file

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

View file

@ -26,38 +26,25 @@ export class BufferedStream {
this.reader = stream.getReader();
}
/**
*
* @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();
private async readSource() {
const { done, value } = await this.reader.read();
if (done) {
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) {
return array;
}
@ -76,11 +63,7 @@ export class BufferedStream {
}
while (length > 0) {
const { done, value: array } = await this.reader.read();
if (done) {
throw new BufferedStreamEndedError();
}
const array = await this.readSource();
if (array.byteLength === length) {
result.set(array, index);
return result;
@ -102,6 +85,31 @@ export class BufferedStream {
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
* all data from the wrapped stream.

View file

@ -645,13 +645,11 @@ struct.field(
### Relationship between types
A `Struct` is a map between keys and `StructFieldDefinition`s.
A `StructValue` is a map between keys and `StructFieldValue`s.
A `Struct` can create (deserialize) multiple `StructValue`s with same field definitions.
Each time a `Struct` deserialize, each `StructFieldDefinition` in it creates exactly one `StructFieldValue` to be put into the `StructValue`.
* `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.
* `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.
* Result of `Struct#deserialize()`: A map between field names and results of `StructFieldValue#get()`.
### `StructFieldDefinition`

View file

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

View file

@ -1,3 +1,5 @@
import type { ValueOrPromise } from "../utils.js";
export interface StructDeserializeStream {
/**
* 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
* (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");
@ -11,9 +11,15 @@ export class StructValue {
/**
* 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(
this.value,
STRUCT_VALUE_SYMBOL,
@ -31,8 +37,8 @@ export class StructValue {
this.fieldValues[name] = fieldValue;
// PERF: `Object.defineProperty` is slow
if (fieldValue.get !== StructFieldValue.prototype.get ||
fieldValue.set !== StructFieldValue.prototype.set) {
// use normal property when possible
if (fieldValue.hasCustomAccessors) {
Object.defineProperty(this.value, name, {
configurable: true,
enumerable: true,

View file

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

View file

@ -1,185 +1,106 @@
enum State {
Pending,
Fulfilled,
Rejected,
export interface SyncPromise<T> {
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
): SyncPromise<TResult1 | TResult2>;
valueOrPromise(): T | PromiseLike<T>;
}
function fulfilledThen<T, TResult1 = T>(
this: SyncPromise<T>,
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
): SyncPromise<TResult1> {
if (onfulfilled) {
return SyncPromise.try(() => onfulfilled(this.result as T));
}
return this as unknown as SyncPromise<TResult1>;
interface SyncPromiseStatic {
reject<T = never>(reason?: any): SyncPromise<T>;
resolve(): SyncPromise<void>;
resolve<T>(value: T | PromiseLike<T>): SyncPromise<T>;
try<T>(executor: () => T | PromiseLike<T>): SyncPromise<T>;
}
function rejectedThen<T, TResult1 = T, TResult2 = never>(
this: SyncPromise<T>,
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): SyncPromise<TResult2> {
if (onrejected) {
return SyncPromise.try(() => onrejected(this.result));
export const SyncPromise: SyncPromiseStatic = {
reject<T = never>(reason?: any): SyncPromise<T> {
return new RejectedSyncPromise(reason);
},
resolve<T>(value?: T | PromiseLike<T>): SyncPromise<T> {
if (
typeof value === 'object' &&
value !== null &&
typeof (value as PromiseLike<T>).then === 'function'
) {
return new PendingSyncPromise(value as PromiseLike<T>);
} else {
return new ResolvedSyncPromise(value as T);
}
return this as unknown as SyncPromise<TResult2>;
}
function fulfilledValue<T>(
this: SyncPromise<T>,
) {
return this.result as T;
}
function rejectedValue<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<T>(executor: () => T | PromiseLike<T>): SyncPromise<T> {
try {
return SyncPromise.resolve(executor());
} catch (e) {
return SyncPromise.reject(e);
}
}
};
public state: State = State.Pending;
public result: unknown;
public promise: PromiseLike<T> | undefined;
class PendingSyncPromise<T> implements SyncPromise<T> {
private promise: PromiseLike<T>;
public constructor(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void) {
if (executor === INTERNAL_CREATED) {
return;
public constructor(promise: PromiseLike<T>) {
this.promise = promise;
}
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 {
executor(
(value: T | PromiseLike<T>) => {
if (settled) { return; }
settled = true;
if (!sync) {
promiseResolve(value);
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>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
): SyncPromise<TResult1 | TResult2> {
// The result promise isn't really a `SyncPromise`,
// but we attach a `valueOrPromise` to it,
// 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);
) {
return new PendingSyncPromise<TResult1 | TResult2>(
this.promise.then(onfulfilled, onrejected)
);
}
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;
}
}