mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Add port of ConcurrentMap from .NET
This commit is contained in:
@@ -123,7 +123,7 @@ export async function runConsoleTests(runJs, defaultReporter, runInParallel, opt
|
||||
if (structs) {
|
||||
args.unshift("--harmony-struct");
|
||||
args.unshift("--shared-string-table");
|
||||
args.unshift("--enable-source-map");
|
||||
args.unshift("--enable-source-maps");
|
||||
}
|
||||
|
||||
/** @type {number | undefined} */
|
||||
|
||||
@@ -10,6 +10,26 @@ export * from "../tracing";
|
||||
export * from "../types";
|
||||
export * from "../sys";
|
||||
export * from "../workerThreads";
|
||||
export * from "../sharing/structs/fakeSharedStruct";
|
||||
export * from "../sharing/structs/identifiableStruct";
|
||||
export * from "../sharing/structs/shareable";
|
||||
export * from "../sharing/structs/sharedStruct";
|
||||
export * from "../sharing/structs/taggedStruct";
|
||||
export * from "../sharing/structs/wrapper";
|
||||
export * from "../sharing/collections/hashData";
|
||||
export * from "../sharing/collections/sharedLinkedList";
|
||||
export * from "../sharing/collections/concurrentMap";
|
||||
export * from "../sharing/collections/sharedMap";
|
||||
export * from "../sharing/collections/sharedResizableArray";
|
||||
export * from "../sharing/collections/sharedSet";
|
||||
export * from "../sharing/collections/xxhash32";
|
||||
export * from "../sharing/sharedDiagnostics";
|
||||
export * from "../sharing/sharedNode";
|
||||
export * from "../sharing/sharedNodeAdapter";
|
||||
export * from "../sharing/sharedNodeArray";
|
||||
export * from "../sharing/sharedObjectAllocator";
|
||||
export * from "../sharing/sharedParserState";
|
||||
export * from "../sharing/sharedSymbol";
|
||||
export * from "../threading/condition";
|
||||
export * from "../threading/countdownEvent";
|
||||
export * from "../threading/lockable";
|
||||
@@ -20,24 +40,6 @@ export * from "../threading/sharedLockable";
|
||||
export * from "../threading/sharedMutex";
|
||||
export * from "../threading/threadPool";
|
||||
export * from "../threading/uniqueLock";
|
||||
export * from "../sharing/collections/hashData";
|
||||
export * from "../sharing/collections/sharedLinkedList";
|
||||
export * from "../sharing/collections/sharedMap";
|
||||
export * from "../sharing/collections/sharedResizableArray";
|
||||
export * from "../sharing/collections/sharedSet";
|
||||
export * from "../sharing/collections/xxhash32";
|
||||
export * from "../sharing/structs/fakeSharedStruct";
|
||||
export * from "../sharing/structs/identifiableStruct";
|
||||
export * from "../sharing/structs/shareable";
|
||||
export * from "../sharing/structs/sharedStruct";
|
||||
export * from "../sharing/structs/taggedStruct";
|
||||
export * from "../sharing/sharedDiagnostics";
|
||||
export * from "../sharing/sharedNode";
|
||||
export * from "../sharing/sharedNodeAdapter";
|
||||
export * from "../sharing/sharedNodeArray";
|
||||
export * from "../sharing/sharedObjectAllocator";
|
||||
export * from "../sharing/sharedParserState";
|
||||
export * from "../sharing/sharedSymbol";
|
||||
export * from "../path";
|
||||
export * from "../diagnosticInformationMap.generated";
|
||||
export * from "../scanner";
|
||||
|
||||
+100
-45
@@ -1,23 +1,19 @@
|
||||
import {
|
||||
__String,
|
||||
CharacterCodes,
|
||||
Comparer,
|
||||
Comparison,
|
||||
Debug,
|
||||
EqualityComparer,
|
||||
isTaggedStruct,
|
||||
isWhiteSpaceLike,
|
||||
MapLike,
|
||||
Queue,
|
||||
SharedNodeArray,
|
||||
Queue, SharedNodeArray,
|
||||
SortedArray,
|
||||
SortedReadonlyArray,
|
||||
TextSpan,
|
||||
TextSpan
|
||||
} from "./_namespaces/ts";
|
||||
import { SharedNodeBase } from "./sharing/sharedNode";
|
||||
import { SharedNode } from "./sharing/sharedNode";
|
||||
import { isShareableNonPrimitive, isSharedArray } from "./sharing/structs/shareable";
|
||||
import { Tag } from "./sharing/structs/taggedStruct";
|
||||
|
||||
import { isTaggedStruct, Tag } from "./sharing/structs/taggedStruct";
|
||||
|
||||
/** @internal */
|
||||
export const emptyArray: never[] = [] as never[];
|
||||
@@ -27,7 +23,23 @@ export const emptyMap: ReadonlyMap<never, never> = new Map<never, never>();
|
||||
export const emptySet: ReadonlySet<never> = new Set<never>();
|
||||
|
||||
/** @internal */
|
||||
export function length(array: readonly any[] | undefined): number {
|
||||
export type Sequence<T> =
|
||||
| readonly T[]
|
||||
| (T extends Shareable ? SharedArray<T> : never)
|
||||
| (T extends SharedNode ? SharedNodeArray<T> : never)
|
||||
;
|
||||
|
||||
function getArrayLike<T>(sequence: Sequence<T>): ArrayLike<T>;
|
||||
function getArrayLike<T>(sequence: Sequence<T> | undefined): ArrayLike<T> | undefined;
|
||||
function getArrayLike<T>(sequence: Sequence<T> | undefined): ArrayLike<T> | undefined {
|
||||
return !sequence || isArray(sequence) || isSharedArray(sequence) ? sequence :
|
||||
sequence instanceof SharedNodeArray ? sequence.items :
|
||||
undefined;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function length(sequence: Sequence<any> | undefined): number {
|
||||
const array = getArrayLike(sequence);
|
||||
return array ? array.length : 0;
|
||||
}
|
||||
|
||||
@@ -38,9 +50,11 @@ export function length(array: readonly any[] | undefined): number {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function forEach<T, U>(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
|
||||
export function forEach<T, U>(sequence: Sequence<T> | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const result = callback(array[i], i);
|
||||
if (result) {
|
||||
return result;
|
||||
@@ -55,7 +69,8 @@ export function forEach<T, U>(array: readonly T[] | undefined, callback: (elemen
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function forEachRight<T, U>(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
|
||||
export function forEachRight<T, U>(sequence: Sequence<T> | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array) {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
const result = callback(array[i], i);
|
||||
@@ -72,12 +87,14 @@ export function forEachRight<T, U>(array: readonly T[] | undefined, callback: (e
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function firstDefined<T, U>(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
|
||||
export function firstDefined<T, U>(sequence: Sequence<T> | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const result = callback(array[i], i);
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
@@ -111,8 +128,10 @@ export function reduceLeftIterator<T, U>(iterator: Iterable<T> | undefined, f: (
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function zipWith<T, U, V>(arrayA: readonly T[], arrayB: readonly U[], callback: (a: T, b: U, index: number) => V): V[] {
|
||||
export function zipWith<T, U, V>(sequenceA: Sequence<T>, sequenceB: Sequence<U>, callback: (a: T, b: U, index: number) => V): V[] {
|
||||
const result: V[] = [];
|
||||
const arrayA = getArrayLike(sequenceA);
|
||||
const arrayB = getArrayLike(sequenceB);
|
||||
Debug.assertEqual(arrayA.length, arrayB.length);
|
||||
for (let i = 0; i < arrayA.length; i++) {
|
||||
result.push(callback(arrayA[i], arrayB[i], i));
|
||||
@@ -149,10 +168,14 @@ export function every<T, U extends T>(array: readonly T[], callback: (element: T
|
||||
/** @internal */
|
||||
export function every<T, U extends T>(array: readonly T[] | undefined, callback: (element: T, index: number) => element is U): array is readonly U[] | undefined;
|
||||
/** @internal */
|
||||
export function every<T>(array: readonly T[] | undefined, callback: (element: T, index: number) => boolean): boolean;
|
||||
export function every<T>(array: readonly T[] | undefined, callback: (element: T, index: number) => boolean): boolean {
|
||||
export function every<T, U extends T>(sequence: Sequence<T> | undefined, callback: (element: T, index: number) => element is U): sequence is Sequence<U> | undefined;
|
||||
/** @internal */
|
||||
export function every<T>(sequence: Sequence<T> | undefined, callback: (element: T, index: number) => boolean): boolean;
|
||||
export function every<T>(sequence: Sequence<T> | undefined, callback: (element: T, index: number) => boolean): boolean {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (!callback(array[i], i)) {
|
||||
return false;
|
||||
}
|
||||
@@ -167,13 +190,15 @@ export function every<T>(array: readonly T[] | undefined, callback: (element: T,
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function find<T, U extends T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => element is U, startIndex?: number): U | undefined;
|
||||
export function find<T, U extends T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => element is U, startIndex?: number): U | undefined;
|
||||
/** @internal */
|
||||
export function find<T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined;
|
||||
export function find<T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined;
|
||||
/** @internal */
|
||||
export function find<T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined {
|
||||
export function find<T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array === undefined) return undefined;
|
||||
for (let i = startIndex ?? 0; i < array.length; i++) {
|
||||
const length = array.length;
|
||||
for (let i = startIndex ?? 0; i < length; i++) {
|
||||
const value = array[i];
|
||||
if (predicate(value, i)) {
|
||||
return value;
|
||||
@@ -183,11 +208,12 @@ export function find<T>(array: readonly T[] | undefined, predicate: (element: T,
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function findLast<T, U extends T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => element is U, startIndex?: number): U | undefined;
|
||||
export function findLast<T, U extends T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => element is U, startIndex?: number): U | undefined;
|
||||
/** @internal */
|
||||
export function findLast<T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined;
|
||||
export function findLast<T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined;
|
||||
/** @internal */
|
||||
export function findLast<T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined {
|
||||
export function findLast<T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): T | undefined {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array === undefined) return undefined;
|
||||
for (let i = startIndex ?? array.length - 1; i >= 0; i--) {
|
||||
const value = array[i];
|
||||
@@ -203,9 +229,11 @@ export function findLast<T>(array: readonly T[] | undefined, predicate: (element
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findIndex<T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
|
||||
export function findIndex<T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array === undefined) return -1;
|
||||
for (let i = startIndex ?? 0; i < array.length; i++) {
|
||||
const length = array.length;
|
||||
for (let i = startIndex ?? 0; i < length; i++) {
|
||||
if (predicate(array[i], i)) {
|
||||
return i;
|
||||
}
|
||||
@@ -214,7 +242,8 @@ export function findIndex<T>(array: readonly T[] | undefined, predicate: (elemen
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function findLastIndex<T>(array: readonly T[] | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
|
||||
export function findLastIndex<T>(sequence: Sequence<T> | undefined, predicate: (element: T, index: number) => boolean, startIndex?: number): number {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array === undefined) return -1;
|
||||
for (let i = startIndex ?? array.length - 1; i >= 0; i--) {
|
||||
if (predicate(array[i], i)) {
|
||||
@@ -230,8 +259,10 @@ export function findLastIndex<T>(array: readonly T[] | undefined, predicate: (el
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findMap<T, U>(array: readonly T[], callback: (element: T, index: number) => U | undefined): U {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
export function findMap<T, U>(sequence: Sequence<T>, callback: (element: T, index: number) => U | undefined): U {
|
||||
const array = getArrayLike(sequence)!;
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const result = callback(array[i], i);
|
||||
if (result) {
|
||||
return result;
|
||||
@@ -241,10 +272,12 @@ export function findMap<T, U>(array: readonly T[], callback: (element: T, index:
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function contains<T>(array: readonly T[] | undefined, value: T, equalityComparer: EqualityComparer<T> = equateValues): boolean {
|
||||
export function contains<T>(sequence: Sequence<T> | undefined, value: T, equalityComparer: EqualityComparer<T> = equateValues): boolean {
|
||||
const array = getArrayLike(sequence);
|
||||
if (array) {
|
||||
for (const v of array) {
|
||||
if (equalityComparer(v, value)) {
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (equalityComparer(array[i], value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -253,8 +286,10 @@ export function contains<T>(array: readonly T[] | undefined, value: T, equalityC
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function arraysEqual<T>(a: readonly T[], b: readonly T[], equalityComparer: EqualityComparer<T> = equateValues): boolean {
|
||||
return a.length === b.length && a.every((x, i) => equalityComparer(x, b[i]));
|
||||
export function arraysEqual<T>(sequenceA: Sequence<T>, sequenceB: Sequence<T>, equalityComparer: EqualityComparer<T> = equateValues): boolean {
|
||||
const arrayA = getArrayLike(sequenceA);
|
||||
const arrayB = getArrayLike(sequenceB);
|
||||
return arrayA.length === arrayB.length && Array.prototype.every.call(arrayA, (x: T, i: number) => equalityComparer(x, arrayB[i]));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -268,10 +303,12 @@ export function indexOfAnyCharCode(text: string, charCodes: readonly number[], s
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function countWhere<T>(array: readonly T[] | undefined, predicate: (x: T, i: number) => boolean): number {
|
||||
export function countWhere<T>(sequence: Sequence<T> | undefined, predicate: (x: T, i: number) => boolean): number {
|
||||
let count = 0;
|
||||
const array = getArrayLike(sequence);
|
||||
if (array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const v = array[i];
|
||||
if (predicate(v, i)) {
|
||||
count++;
|
||||
@@ -665,22 +702,23 @@ export function mapEntries<K1, V1, K2, V2>(map: ReadonlyMap<K1, V1> | undefined,
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function some<T>(array: readonly T[] | SharedNodeArray<Extract<T, SharedNodeBase>> | undefined): array is readonly T[] | SharedNodeArray<Extract<T, SharedNodeBase>>;
|
||||
export function some<T>(array: readonly T[] | SharedNodeArray<Extract<T, SharedNode>> | undefined): array is readonly T[] | SharedNodeArray<Extract<T, SharedNode>>;
|
||||
/** @internal */
|
||||
export function some<T>(array: readonly T[] | SharedNodeArray<Extract<T, SharedNodeBase>> | undefined, predicate: (value: T) => boolean): boolean;
|
||||
export function some<T>(array: readonly T[] | SharedNodeArray<Extract<T, SharedNode>> | undefined, predicate: (value: T) => boolean): boolean;
|
||||
/** @internal */
|
||||
export function some<T>(array: readonly T[] | SharedNodeArray<Extract<T, SharedNodeBase>> | undefined, predicate?: (value: T) => boolean): boolean {
|
||||
export function some<T>(array: readonly T[] | SharedNodeArray<Extract<T, SharedNode>> | undefined, predicate?: (value: T) => boolean): boolean {
|
||||
if (array) {
|
||||
if (predicate) {
|
||||
const iterable = isTaggedStruct(array, Tag.NodeArray) ? SharedNodeArray.values(array) : array;
|
||||
for (const v of iterable) {
|
||||
if (predicate(v)) {
|
||||
const arrayLike = isTaggedStruct(array, Tag.NodeArray) ? (array as SharedNodeArray<any>).items : array;
|
||||
const length = arrayLike.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (predicate(arrayLike[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return (array instanceof SharedNodeArray ? array.items.length : array.length) > 0;
|
||||
return (isTaggedStruct(array, Tag.NodeArray) ? (array as SharedNodeArray<any>).items.length : array.length) > 0;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -1846,6 +1884,13 @@ export function isArray(value: any): value is readonly unknown[] {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function isIterable(value: any): value is Iterable<any> {
|
||||
return value !== undefined && !isNull(value) && Symbol.iterator in value
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toArray<T>(value: T | T[] | SharedArray<Extract<T, Shareable>>): T[];
|
||||
/** @internal */
|
||||
@@ -1855,6 +1900,11 @@ export function toArray<T>(value: T | T[] | SharedArray<Extract<T, Shareable>>):
|
||||
return isSharedArray(value) ? Array.from(value) : isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function iterateValues<T>(values: ArrayLike<T>): IterableIterator<T> {
|
||||
return Array.prototype.values.call(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether a value is string
|
||||
*
|
||||
@@ -1888,7 +1938,7 @@ export function cast<TOut extends TIn, TIn = any>(value: TIn | undefined, test:
|
||||
if ("__tag__" in valueArg) {
|
||||
const tag = valueArg.__tag__ as Tag;
|
||||
if (tag === Tag.Node) {
|
||||
const kind = (valueArg as SharedNodeBase).kind;
|
||||
const kind = (valueArg as SharedNode).kind;
|
||||
valueArg = `[object SharedNodeBase(${Debug.formatSyntaxKind(kind)})]`;
|
||||
}
|
||||
else {
|
||||
@@ -2947,3 +2997,8 @@ export class Lazy<T> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function dispose(obj: Disposable | undefined) {
|
||||
obj?.[Symbol.dispose]();
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ import {
|
||||
WithStatement,
|
||||
YieldExpression,
|
||||
} from "../_namespaces/ts";
|
||||
import { SharedNodeBase } from "../sharing/sharedNode";
|
||||
import { SharedNode } from "../sharing/sharedNode";
|
||||
import { SharedNodeArray } from "../sharing/sharedNodeArray";
|
||||
import { isShareableNonPrimitive } from "../sharing/structs/shareable";
|
||||
|
||||
@@ -7182,7 +7182,7 @@ function propagateChildrenFlags(children: NodeArray<Node> | undefined): Transfor
|
||||
return children ? children.transformFlags : TransformFlags.None;
|
||||
}
|
||||
|
||||
function aggregateChildrenFlags(children: MutableNodeArray<Node> | SharedNodeArray<SharedNodeBase>) {
|
||||
function aggregateChildrenFlags(children: MutableNodeArray<Node> | SharedNodeArray<SharedNode>) {
|
||||
let subtreeFlags = TransformFlags.None;
|
||||
const values = children instanceof SharedNodeArray ? Array.from(children.items) : children;
|
||||
for (const child of values) {
|
||||
|
||||
@@ -408,7 +408,7 @@ import {
|
||||
} from "./_namespaces/ts";
|
||||
import * as performance from "./_namespaces/ts.performance";
|
||||
import { SharedDiagnostic, SharedDiagnosticMessageChain, SharedDiagnosticRelatedInformation, SharedDiagnosticWithLocation } from "./sharing/sharedDiagnostics";
|
||||
import { SharedAmdDependency, SharedCheckJsDirective, SharedCommentDirective, SharedCommentRange, SharedFileReference, SharedNodeBase, SharedPragma, SharedPragmaArguments, SharedPragmaSpan, SharedSourceFile, SharedTextRange } from "./sharing/sharedNode";
|
||||
import { SharedAmdDependency, SharedCheckJsDirective, SharedCommentDirective, SharedCommentRange, SharedFileReference, SharedNode, SharedPragma, SharedPragmaArguments, SharedPragmaSpan, SharedSourceFile, SharedTextRange } from "./sharing/sharedNode";
|
||||
import { SharedNodeArray } from "./sharing/sharedNodeArray";
|
||||
import { isShareableNonPrimitive } from "./sharing/structs/shareable";
|
||||
import { Tag, TaggedStruct } from "./sharing/structs/taggedStruct";
|
||||
@@ -461,8 +461,10 @@ function visitNodes<T>(cbNode: (node: Node) => T, cbNodes: ((node: NodeArray<Nod
|
||||
if (cbNodes) {
|
||||
return cbNodes(nodes);
|
||||
}
|
||||
for (const node of nodes) {
|
||||
const result = cbNode(node);
|
||||
const array = nodes instanceof SharedNodeArray ? nodes.items : nodes;
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const result = cbNode(array[i]);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
@@ -1912,8 +1914,8 @@ namespace Parser {
|
||||
Debug.assert(!node.jsDoc); // Should only be called once per node
|
||||
const jsDoc = mapDefined(getJSDocCommentRanges(node, sourceText), comment => JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos));
|
||||
if (jsDoc.length) {
|
||||
if (node instanceof SharedNodeBase) {
|
||||
node.jsDoc = shareArray(jsDoc, n => n as unknown as SharedNodeBase) as unknown as JSDoc[];
|
||||
if (node instanceof SharedNode) {
|
||||
node.jsDoc = shareArray(jsDoc, n => n as unknown as SharedNode) as unknown as JSDoc[];
|
||||
}
|
||||
else {
|
||||
node.jsDoc = jsDoc;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "./symbolDisposeShim";
|
||||
|
||||
import {
|
||||
Debug,
|
||||
noop,
|
||||
@@ -64,6 +66,48 @@ const marks = new Map<string, number>();
|
||||
const counts = new Map<string, number>();
|
||||
const durations = new Map<string, number>();
|
||||
|
||||
class MeasureActivity implements Disposable {
|
||||
active = false;
|
||||
measureName!: string;
|
||||
startMarkName!: string;
|
||||
endMarkName: string | undefined;
|
||||
|
||||
[Symbol.dispose](): void {
|
||||
if (this.active) {
|
||||
this.active = false;
|
||||
if (this.endMarkName) {
|
||||
mark(this.endMarkName);
|
||||
}
|
||||
measure(this.measureName, this.startMarkName, this.endMarkName);
|
||||
if (measureActivityPool.length < measureActivityPoolSize) {
|
||||
measureActivityPool.push(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const measureActivityPoolSize = 3;
|
||||
const measureActivityPool: MeasureActivity[] = [];
|
||||
|
||||
/**
|
||||
* Marks a performance event when called, and measures the performance event when the result is disposed.
|
||||
* @internal
|
||||
*/
|
||||
export function measureActivity(measureName: string, startMarkName?: string, endMarkName?: string): Disposable | undefined {
|
||||
if (!enabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activity = measureActivityPool.pop() ?? new MeasureActivity();
|
||||
Debug.assert(!activity.active);
|
||||
activity.measureName = measureName;
|
||||
activity.startMarkName = startMarkName ?? `${measureName}-start-${Date.now()}`;
|
||||
activity.endMarkName = endMarkName;
|
||||
activity.active = true;
|
||||
mark(activity.startMarkName);
|
||||
return activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a performance event.
|
||||
*
|
||||
|
||||
+114
-69
@@ -342,11 +342,10 @@ import {
|
||||
zipToModeAwareCache
|
||||
} from "./_namespaces/ts";
|
||||
import * as performance from "./_namespaces/ts.performance";
|
||||
import { SharedMap } from "./sharing/collections/sharedMap";
|
||||
import { ConcurrentMap } from "./sharing/collections/concurrentMap";
|
||||
import { adoptSharedSourceFile } from "./sharing/sharedNodeAdapter";
|
||||
import { SharedParserState, SharedSourceFileEntry } from "./sharing/sharedParserState";
|
||||
import { Condition } from "./threading/condition";
|
||||
import { SharedLock } from "./threading/sharedLock";
|
||||
import { UniqueLock } from "./threading/uniqueLock";
|
||||
|
||||
export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName = "tsconfig.json"): string | undefined {
|
||||
@@ -412,6 +411,101 @@ export function createCompilerHost(options: CompilerOptions, setParentNodes?: bo
|
||||
return createCompilerHostWorker(options, setParentNodes);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function makeCompilerHostParallel(
|
||||
host: CompilerHost,
|
||||
threadPool: ThreadPool,
|
||||
setParentNodes: boolean | undefined,
|
||||
): CompilerHost {
|
||||
Debug.assert(!host.threadPool && !host.requestSourceFile, "Compiler host may already be parallelized.");
|
||||
|
||||
const sharedEntryMap = new Map<SharedSourceFileEntry, SourceFile | false>();
|
||||
const parserState = new SharedParserState();
|
||||
const savedGetSourceFile = host.getSourceFile;
|
||||
host.threadPool = threadPool;
|
||||
host.requestSourceFile = (fileName, languageVersionOrOptions, shouldCreateNewSourceFile) => {
|
||||
using _measure = performance.measureActivity("Parallel: Request Source File", "beforeParserPausedRequest", "afterParserPausedRequest");
|
||||
|
||||
if (!ConcurrentMap.has(parserState.files, fileName)) {
|
||||
const entry = new SharedSourceFileEntry(
|
||||
parserState,
|
||||
!!host.createHash,
|
||||
!!setParentNodes,
|
||||
fileName,
|
||||
typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.languageVersion : languageVersionOrOptions,
|
||||
typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.impliedNodeFormat : undefined,
|
||||
shouldCreateNewSourceFile);
|
||||
if (ConcurrentMap.insert(parserState.files, fileName, entry)) {
|
||||
// we inserted the entry, queue a task to parse it
|
||||
threadPool.queueWorkItem("Program.requestSourceFile", entry);
|
||||
}
|
||||
}
|
||||
|
||||
// // quick check before allocating an entry
|
||||
// {
|
||||
// using _ = new SharedLock(parserState.sharedMutex);
|
||||
// if (SharedMap.has(parserState.files, fileName)) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// const entry = new SharedSourceFileEntry(
|
||||
// parserState,
|
||||
// !!host.createHash,
|
||||
// !!setParentNodes,
|
||||
// fileName,
|
||||
// typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.languageVersion : languageVersionOrOptions,
|
||||
// typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.impliedNodeFormat : undefined,
|
||||
// shouldCreateNewSourceFile);
|
||||
|
||||
// using _ = new UniqueLock(parserState.sharedMutex);
|
||||
// if (!SharedMap.has(parserState.files, fileName)) {
|
||||
// SharedMap.set(parserState.files, fileName, entry);
|
||||
// // we inserted the entry, queue a task to parse it
|
||||
// threadPool.queueWorkItem("Program.requestSourceFile", entry);
|
||||
// }
|
||||
};
|
||||
host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
||||
using _ = performance.measureActivity("Parallel: Get Source File", "beforeParserPausedRead", "afterParserPausedRead");
|
||||
|
||||
const sharedFileEntry = ConcurrentMap.get(parserState.files, fileName);
|
||||
|
||||
// let sharedFileEntry: SharedSourceFileEntry | undefined;
|
||||
// {
|
||||
// using _ = new SharedLock(parserState.sharedMutex);
|
||||
// sharedFileEntry = SharedMap.get(parserState.files, fileName);
|
||||
// }
|
||||
|
||||
if (sharedFileEntry) {
|
||||
let file = sharedEntryMap.get(sharedFileEntry);
|
||||
if (file !== undefined) {
|
||||
return file || undefined;
|
||||
}
|
||||
|
||||
// wait until the file has actually been parsed before we proceed.
|
||||
{
|
||||
using lck = new UniqueLock(sharedFileEntry.fileMutex);
|
||||
Condition.wait(sharedFileEntry.fileCondition, lck, () => sharedFileEntry!.done || sharedFileEntry!.error);
|
||||
}
|
||||
|
||||
// if the worker thread reported a crash, we should crash the main thread.
|
||||
if (sharedFileEntry.error) {
|
||||
host.threadPool?.abort();
|
||||
sys.exit(-1);
|
||||
}
|
||||
|
||||
file = sharedFileEntry.file && adoptSharedSourceFile(sharedFileEntry.file, parseNodeFactory);
|
||||
if (file) {
|
||||
file.bindDiagnostics = [];
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
return savedGetSourceFile.call(host, fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
||||
};
|
||||
return host;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createGetSourceFile(
|
||||
readFile: ProgramHost<any>["readFile"],
|
||||
@@ -483,25 +577,6 @@ export function createWriteFileMeasuringIO(
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createRequestSourceFile(threadPool: ThreadPool, setParentNodes: boolean | undefined): CompilerHost["requestSourceFile"] {
|
||||
return (parserState, fileName, languageVersionOrOptions, shouldCreateNewSourceFile, setFileVersion = false) => {
|
||||
using _ = new UniqueLock(parserState.sharedMutex);
|
||||
if (!SharedMap.has(parserState.files, fileName)) {
|
||||
const entry = new SharedSourceFileEntry(
|
||||
parserState,
|
||||
setFileVersion,
|
||||
!!setParentNodes,
|
||||
fileName,
|
||||
typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.languageVersion : languageVersionOrOptions,
|
||||
typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.impliedNodeFormat : undefined,
|
||||
shouldCreateNewSourceFile);
|
||||
SharedMap.set(parserState.files, fileName, entry);
|
||||
threadPool.queueWorkItem("Program.requestSourceFile", entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createCompilerHostWorker(options: CompilerOptions, setParentNodes?: boolean, system: System = sys, workerThreads?: WorkerThreadsHost, threadPool?: ThreadPool, overideObjectAllocator?: ObjectAllocator): CompilerHost {
|
||||
const existingDirectories = new Map<string, boolean>();
|
||||
@@ -525,7 +600,6 @@ export function createCompilerHostWorker(options: CompilerOptions, setParentNode
|
||||
const newLine = getNewLineCharacter(options);
|
||||
const realpath = system.realpath && ((path: string) => system.realpath!(path));
|
||||
const compilerHost: CompilerHost = {
|
||||
requestSourceFile: threadPool && createRequestSourceFile(threadPool, setParentNodes),
|
||||
getSourceFile: createGetSourceFile(fileName => compilerHost.readFile(fileName), () => options, setParentNodes, overideObjectAllocator),
|
||||
getDefaultLibLocation,
|
||||
getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)),
|
||||
@@ -549,8 +623,10 @@ export function createCompilerHostWorker(options: CompilerOptions, setParentNode
|
||||
createDirectory: d => system.createDirectory(d),
|
||||
createHash: maybeBind(system, system.createHash),
|
||||
workerThreads,
|
||||
threadPool,
|
||||
};
|
||||
if (threadPool) {
|
||||
makeCompilerHostParallel(compilerHost, threadPool, setParentNodes);
|
||||
}
|
||||
return compilerHost;
|
||||
}
|
||||
|
||||
@@ -3613,18 +3689,6 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
|
||||
return redirect;
|
||||
}
|
||||
|
||||
// Get source file from normalized fileName
|
||||
function* findSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: FileIncludeReason, packageId: PackageId | undefined, currentNodeModulesDepth: number): Generator<undefined, SourceFile | undefined> {
|
||||
tracing?.push(tracing.Phase.Program, "findSourceFile", {
|
||||
fileName,
|
||||
isDefaultLib: isDefaultLib || undefined,
|
||||
fileIncludeKind: (FileIncludeKind as any)[reason.kind],
|
||||
});
|
||||
const result = yield* findSourceFileWorker(fileName, isDefaultLib, ignoreNoDefaultLib, reason, packageId, currentNodeModulesDepth);
|
||||
tracing?.pop();
|
||||
return result;
|
||||
}
|
||||
|
||||
function getCreateSourceFileOptions(fileName: string, moduleResolutionCache: ModuleResolutionCache | undefined, host: CompilerHost, options: CompilerOptions): CreateSourceFileOptions {
|
||||
// It's a _little odd_ that we can't set `impliedNodeFormat` until the program step - but it's the first and only time we have a resolution cache
|
||||
// and a freshly made source file node on hand at the same time, and we need both to set the field. Persisting the resolution cache all the way
|
||||
@@ -3637,7 +3701,14 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
|
||||
{ languageVersion, impliedNodeFormat: result, setExternalModuleIndicator };
|
||||
}
|
||||
|
||||
function* findSourceFileWorker(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: FileIncludeReason, packageId: PackageId | undefined, currentNodeModulesDepth: number): Generator<undefined, SourceFile | undefined> {
|
||||
// Get source file from normalized fileName
|
||||
function* findSourceFile(fileName: string, isDefaultLib: boolean, ignoreNoDefaultLib: boolean, reason: FileIncludeReason, packageId: PackageId | undefined, currentNodeModulesDepth: number): Generator<undefined, SourceFile | undefined> {
|
||||
using _ = tracing?.traceActivity(tracing.Phase.Program, "findSourceFile", {
|
||||
fileName,
|
||||
isDefaultLib: isDefaultLib || undefined,
|
||||
fileIncludeKind: (FileIncludeKind as any)[reason.kind],
|
||||
});
|
||||
|
||||
const path = toPath(fileName);
|
||||
if (useSourceOfProjectReferenceRedirect) {
|
||||
let source = getSourceOfProjectReferenceRedirect(path);
|
||||
@@ -3736,38 +3807,12 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
|
||||
// We haven't looked for this file, do so now and cache result
|
||||
const sourceFileOptions = getCreateSourceFileOptions(fileName, moduleResolutionCache, host, options);
|
||||
|
||||
let fileParsedInBackground = false;
|
||||
let file: SourceFile | undefined;
|
||||
if (parserState) {
|
||||
let sharedFileEntry: SharedSourceFileEntry | undefined;
|
||||
{
|
||||
using _ = new SharedLock(parserState.sharedMutex);
|
||||
sharedFileEntry = SharedMap.get(parserState.files, fileName);
|
||||
}
|
||||
if (sharedFileEntry) {
|
||||
using lck = new UniqueLock(sharedFileEntry.fileMutex);
|
||||
Condition.wait(sharedFileEntry.fileCondition, lck, () => sharedFileEntry!.done || sharedFileEntry!.error);
|
||||
if (sharedFileEntry.error) {
|
||||
host.threadPool?.abort();
|
||||
sys.exit(-1);
|
||||
}
|
||||
file = sharedFileEntry.file && adoptSharedSourceFile(sharedFileEntry.file, parseNodeFactory);
|
||||
if (file) {
|
||||
file.bindDiagnostics = [];
|
||||
}
|
||||
fileParsedInBackground = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileParsedInBackground) {
|
||||
file = host.getSourceFile(
|
||||
fileName,
|
||||
sourceFileOptions,
|
||||
hostErrorMessage => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, Diagnostics.Cannot_read_file_0_Colon_1, [fileName, hostErrorMessage]),
|
||||
shouldCreateNewSourceFile,
|
||||
);
|
||||
}
|
||||
|
||||
const file = host.getSourceFile(
|
||||
fileName,
|
||||
sourceFileOptions,
|
||||
hostErrorMessage => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, Diagnostics.Cannot_read_file_0_Colon_1, [fileName, hostErrorMessage]),
|
||||
shouldCreateNewSourceFile,
|
||||
);
|
||||
|
||||
if (packageId) {
|
||||
const packageIdKey = packageIdToString(packageId);
|
||||
@@ -5152,7 +5197,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
|
||||
|
||||
function* requestFile(fileName: string) {
|
||||
if (parserState && host.requestSourceFile) {
|
||||
host.requestSourceFile?.(parserState, fileName, getEmitScriptTarget(options));
|
||||
host.requestSourceFile?.(fileName, getEmitScriptTarget(options));
|
||||
yield;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Shared, SharedStructBase } from "./structs/sharedStruct";
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
export class AtomicValue<T extends Shareable> extends SharedStructBase {
|
||||
@Shared() unsafeValue: T;
|
||||
|
||||
constructor(value: T) {
|
||||
super();
|
||||
this.unsafeValue = value;
|
||||
}
|
||||
|
||||
static load<T extends Shareable>(self: AtomicValue<T>) {
|
||||
return Atomics.load(self, "unsafeValue");
|
||||
}
|
||||
|
||||
static store<T extends Shareable>(self: AtomicValue<T>, value: T) {
|
||||
return Atomics.store(self, "unsafeValue", value);
|
||||
}
|
||||
|
||||
static exchange<T extends Shareable>(self: AtomicValue<T>, value: T) {
|
||||
return Atomics.exchange(self, "unsafeValue", value);
|
||||
}
|
||||
|
||||
static compareExchange<T extends Shareable>(self: AtomicValue<T>, expectedValue: T, replacementValue: T) {
|
||||
return Atomics.compareExchange(self, "unsafeValue", expectedValue, replacementValue);
|
||||
}
|
||||
|
||||
static compareAndSet<T extends Shareable>(self: AtomicValue<T>, expectedValue: T, replacementValue: T) {
|
||||
return expectedValue === AtomicValue.compareExchange(self, expectedValue, replacementValue);
|
||||
}
|
||||
|
||||
static add(self: AtomicValue<number>, value: number) {
|
||||
let currentValue = AtomicValue.load(self);
|
||||
while (!AtomicValue.compareAndSet(self, currentValue, (currentValue + value) >> 0)) {
|
||||
currentValue = AtomicValue.load(self);
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
static sub(self: AtomicValue<number>, value: number) {
|
||||
let currentValue = AtomicValue.load(self);
|
||||
while (!AtomicValue.compareAndSet(self, currentValue, (currentValue - value) >> 0)) {
|
||||
currentValue = AtomicValue.load(self);
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
static increment(self: AtomicValue<number>) {
|
||||
return AtomicValue.add(self, 1);
|
||||
}
|
||||
|
||||
static decrement(self: AtomicValue<number>) {
|
||||
return AtomicValue.sub(self, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class AtomicRef<T extends Shareable> {
|
||||
private _value: AtomicValue<T> | undefined;
|
||||
|
||||
constructor(value: AtomicValue<T> | undefined) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
get disposed() {
|
||||
return !this._value;
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.load(this._value);
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
AtomicValue.store(this._value, value);
|
||||
}
|
||||
|
||||
unsafeGet() {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return this._value.unsafeValue;
|
||||
}
|
||||
|
||||
unsafeSet(value: T) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
this._value.unsafeValue = value;
|
||||
}
|
||||
|
||||
exchange(value: T) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.exchange(this._value, value);
|
||||
}
|
||||
|
||||
compareExchange(expectedValue: T, replacementValue: T) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.compareExchange(this._value, expectedValue, replacementValue);
|
||||
}
|
||||
|
||||
increment(this: AtomicRef<number>) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.increment(this._value);
|
||||
}
|
||||
|
||||
decrement(this: AtomicRef<number>) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.decrement(this._value);
|
||||
}
|
||||
|
||||
add(this: AtomicRef<number>, value: number) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.add(this._value, value);
|
||||
}
|
||||
|
||||
sub(this: AtomicRef<number>, value: number) {
|
||||
if (!this._value) throw new ReferenceError("Object is disposed");
|
||||
return AtomicValue.sub(this._value, value);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this._value = undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
import { iterateValues } from "../../core";
|
||||
import { Debug } from "../../debug";
|
||||
import { sys } from "../../sys";
|
||||
import { Mutex } from "../../threading/mutex";
|
||||
import { ScopedLock } from "../../threading/scopedLock";
|
||||
import { UniqueLock } from "../../threading/uniqueLock";
|
||||
import { AtomicValue } from "../atomicValue";
|
||||
import { Shared, SharedStructBase } from "../structs/sharedStruct";
|
||||
import { Tag, Tagged } from "../structs/taggedStruct";
|
||||
import { StructWrapperTypes } from "../structs/wrapper";
|
||||
import { generateHashSeed, identityHash } from "./hash";
|
||||
import { getPrime } from "./hashData";
|
||||
|
||||
// Portions of the following are derived from .NET Core. See ThirdPartyNoticeText.txt in the root of this repository
|
||||
// for the related license notice.
|
||||
|
||||
@Shared()
|
||||
class Node<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>> extends SharedStructBase {
|
||||
@Shared() readonly key: K;
|
||||
@Shared() value: V;
|
||||
@Shared() readonly next: AtomicValue<Node<K, V> | undefined>;
|
||||
@Shared() readonly hashCode: number;
|
||||
|
||||
constructor(key: K, value: V, hashCode: number, next?: Node<K, V>) {
|
||||
super();
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
this.hashCode = hashCode;
|
||||
this.next = new AtomicValue(next);
|
||||
}
|
||||
}
|
||||
|
||||
@Shared()
|
||||
class Tables<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>> extends SharedStructBase {
|
||||
@Shared() readonly buckets: SharedArray<AtomicValue<Node<K, V> | undefined>>;
|
||||
@Shared() readonly locks: SharedArray<Mutex>;
|
||||
@Shared() readonly countsPerLock: SharedArray<number>;
|
||||
@Shared() readonly seed: number | undefined;
|
||||
|
||||
constructor(buckets: SharedArray<AtomicValue<Node<K, V> | undefined>>, locks: SharedArray<Mutex>, countsPerLock: SharedArray<number>, seed?: number) {
|
||||
super();
|
||||
Debug.assert(Array.prototype.every.call(buckets, x => x !== undefined));
|
||||
Debug.assert(Array.prototype.every.call(locks, x => x !== undefined));
|
||||
Debug.assert(Array.prototype.every.call(countsPerLock, x => x !== undefined));
|
||||
this.buckets = buckets;
|
||||
this.locks = locks;
|
||||
this.countsPerLock = countsPerLock;
|
||||
this.seed = seed;
|
||||
}
|
||||
}
|
||||
|
||||
const DefaultCapacity = 31;
|
||||
const MaxLockNumber = 1024;
|
||||
const SHARED_ARRAY_MAX_LENGTH = 2 ** 14 - 2;
|
||||
const MAX_INT32 = 2 ** 31 - 1;
|
||||
|
||||
const WILDCARD = Symbol();
|
||||
const EXIST = Symbol();
|
||||
const NOT_EXIST = Symbol();
|
||||
|
||||
type WILDCARD = typeof WILDCARD;
|
||||
type EXIST = typeof EXIST;
|
||||
type NOT_EXIST = typeof NOT_EXIST;
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
export class ConcurrentMap<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>> extends Tagged(SharedStructBase, Tag.ConcurrentMap) {
|
||||
declare [StructWrapperTypes]: [
|
||||
[typeof ConcurrentMap, {
|
||||
size(): number;
|
||||
has(key: K): boolean;
|
||||
get(key: K): V | undefined;
|
||||
set(key: K, value: V): void;
|
||||
delete(key: K, expectedValue?: V): V | undefined;
|
||||
insert(key: K, value: V): boolean;
|
||||
replace(key: K, expectedValue: V, replacementValue: V): V | undefined;
|
||||
exchange(key: K, value: V | undefined): V | undefined;
|
||||
compareExchange(key: K, expectedValue: V | undefined, replacementValue: V | undefined): V | undefined;
|
||||
clear(): void;
|
||||
keys(): IterableIterator<K>;
|
||||
values(): IterableIterator<V>;
|
||||
entries(): IterableIterator<[K, V]>;
|
||||
}]
|
||||
];
|
||||
|
||||
@Shared() private readonly _tables: AtomicValue<Tables<K, V>>;
|
||||
@Shared() private _budget: number;
|
||||
@Shared() private readonly _growLockArray: boolean;
|
||||
|
||||
constructor();
|
||||
constructor(items: ConcurrentMap<K, V> | Iterable<[K, V]>);
|
||||
constructor(concurrencyLevel: number, items: ConcurrentMap<K, V> | Iterable<[K, V]>);
|
||||
constructor(concurrencyLevel: number, capacity: number);
|
||||
constructor(concurrencyLevelOrItems?: number | ConcurrentMap<K, V> | Iterable<[K, V]>, capacityOrItems?: number | ConcurrentMap<K, V> | Iterable<[K, V]>) {
|
||||
let concurrencyLevel: number | undefined;
|
||||
let capacity: number | undefined;
|
||||
let items: ConcurrentMap<K, V> | Iterable<[K, V]> | undefined;
|
||||
if (typeof concurrencyLevelOrItems === "object") {
|
||||
items = concurrencyLevelOrItems;
|
||||
concurrencyLevel = sys.cpuCount?.() ?? 1;
|
||||
capacity = ConcurrentMap.getCapacityFromItems(items);
|
||||
}
|
||||
else {
|
||||
concurrencyLevel = concurrencyLevelOrItems ?? sys.cpuCount?.() ?? 1;
|
||||
if (typeof capacityOrItems === "object") {
|
||||
items = capacityOrItems;
|
||||
capacity = ConcurrentMap.getCapacityFromItems(items);
|
||||
}
|
||||
else {
|
||||
capacity = capacityOrItems ?? DefaultCapacity;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.assert(concurrencyLevel >= 1);
|
||||
|
||||
super();
|
||||
|
||||
if (capacity < concurrencyLevel) capacity = concurrencyLevel;
|
||||
capacity = getPrime(capacity);
|
||||
|
||||
const locks = new SharedArray<Mutex>(concurrencyLevel);
|
||||
for (let i = 0; i < concurrencyLevel; i++) {
|
||||
locks[i] = new Mutex();
|
||||
}
|
||||
|
||||
const countsPerLock = new SharedArray<number>(concurrencyLevel);
|
||||
Array.prototype.fill.call(countsPerLock, 0);
|
||||
|
||||
const buckets = new SharedArray<AtomicValue<Node<K, V> | undefined>>(capacity);
|
||||
for (let i = 0; i < capacity; i++) {
|
||||
buckets[i] = new AtomicValue<Node<K, V> | undefined>(/*value*/ undefined);
|
||||
}
|
||||
|
||||
this._tables = new AtomicValue(new Tables(buckets, locks, countsPerLock));
|
||||
this._growLockArray = arguments.length > 0;
|
||||
this._budget = (buckets.length / locks.length) | 0;
|
||||
|
||||
if (items) {
|
||||
if (items instanceof ConcurrentMap) {
|
||||
items = ConcurrentMap.entries(items);
|
||||
}
|
||||
|
||||
for (const [key, value] of items) {
|
||||
const previousValue = ConcurrentMap.compareExchangeInternal(
|
||||
this,
|
||||
AtomicValue.load(this._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
NOT_EXIST,
|
||||
value,
|
||||
/*acquireLock*/ false
|
||||
)
|
||||
Debug.assert(previousValue === undefined, "duplicate key in items");
|
||||
}
|
||||
|
||||
if (this._budget === 0) {
|
||||
const tables = AtomicValue.load(this._tables);
|
||||
this._budget = (tables.buckets.length / tables.locks.length) | 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static getCapacityFromItems<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(items: ConcurrentMap<K, V> | Iterable<[K, V]>) {
|
||||
if (items instanceof ConcurrentMap) return Math.max(DefaultCapacity, ConcurrentMap.size(items));
|
||||
if (items instanceof Map) return Math.max(DefaultCapacity, items.size);
|
||||
if (items instanceof Set) return Math.max(DefaultCapacity, items.size);
|
||||
if (Array.isArray(items)) return Math.max(DefaultCapacity, items.length);
|
||||
return DefaultCapacity;
|
||||
}
|
||||
|
||||
static size<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>) {
|
||||
using _ = new ScopedLock(AtomicValue.load(self._tables).locks);
|
||||
return ConcurrentMap.getSizeNoLocks(self);
|
||||
}
|
||||
|
||||
static has<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K) {
|
||||
return ConcurrentMap.get(self, key) !== undefined;
|
||||
}
|
||||
|
||||
static get<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K) {
|
||||
const tables = AtomicValue.load(self._tables);
|
||||
const hashCode = identityHash(key, tables.seed);
|
||||
for (let node: Node<K, V> | undefined = ConcurrentMap.getBucket(tables, hashCode); node; node = AtomicValue.load(node.next)) {
|
||||
if (hashCode === node.hashCode && key === node.key) {
|
||||
return node.value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static set<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K, value: V) {
|
||||
ConcurrentMap.compareExchangeInternal(
|
||||
self,
|
||||
AtomicValue.load(self._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
WILDCARD,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
static insert<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K, value: V) {
|
||||
return undefined === ConcurrentMap.compareExchangeInternal(
|
||||
self,
|
||||
AtomicValue.load(self._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
NOT_EXIST,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
static replace<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K, expectedValue: V, replacementValue: V) {
|
||||
return undefined !== ConcurrentMap.compareExchangeInternal(
|
||||
self,
|
||||
AtomicValue.load(self._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
expectedValue,
|
||||
replacementValue
|
||||
);
|
||||
}
|
||||
|
||||
static delete<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K, expectedValue?: V) {
|
||||
return ConcurrentMap.compareExchangeInternal(
|
||||
self,
|
||||
AtomicValue.load(self._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
expectedValue ?? EXIST,
|
||||
NOT_EXIST
|
||||
);
|
||||
}
|
||||
|
||||
static exchange<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K, value: V | undefined) {
|
||||
return ConcurrentMap.compareExchangeInternal(
|
||||
self,
|
||||
AtomicValue.load(self._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
WILDCARD,
|
||||
value ?? NOT_EXIST
|
||||
);
|
||||
}
|
||||
|
||||
static compareExchange<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, key: K, expectedValue: V | undefined, replacementValue: V | undefined) {
|
||||
return ConcurrentMap.compareExchangeInternal(
|
||||
self,
|
||||
AtomicValue.load(self._tables),
|
||||
key,
|
||||
/*hashCode*/ undefined,
|
||||
expectedValue ?? NOT_EXIST,
|
||||
replacementValue ?? NOT_EXIST
|
||||
);
|
||||
}
|
||||
|
||||
private static compareExchangeInternal<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(
|
||||
self: ConcurrentMap<K, V>,
|
||||
tables: Tables<K, V>,
|
||||
key: K,
|
||||
hashCode = identityHash(key, tables.seed),
|
||||
expectedValue: V | WILDCARD | EXIST | NOT_EXIST,
|
||||
replacementValue: V | NOT_EXIST,
|
||||
acquireLock = true
|
||||
) {
|
||||
let seed = tables.seed;
|
||||
while (true) {
|
||||
const locks = tables.locks;
|
||||
const { bucket, lockNo } = ConcurrentMap.getBucketAndLock(tables, hashCode);
|
||||
let resizeDesired = false;
|
||||
let forceRehash = false;
|
||||
|
||||
// perf: if we're deleting the value and there are no entries for that bucket, we can exit early. We can also
|
||||
// perf: if we're updating a value that must exist and there are no entries for that bucket, we can exit early.
|
||||
if ((replacementValue === NOT_EXIST || expectedValue === EXIST) && tables.countsPerLock[lockNo] === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// block ensures we release the lock below prior to attempting to grow the table
|
||||
{
|
||||
using _ = acquireLock ? new UniqueLock(locks[lockNo]) : undefined;
|
||||
if (tables !== AtomicValue.load(self._tables)) {
|
||||
tables = AtomicValue.load(self._tables);
|
||||
if (seed !== tables.seed) {
|
||||
seed = tables.seed;
|
||||
hashCode = identityHash(key, tables.seed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let prev: Node<K, V> | undefined;
|
||||
let collisionCount = 0;
|
||||
for (let node: Node<K, V> | undefined = AtomicValue.load(bucket); node; node = AtomicValue.load(node.next)) {
|
||||
Debug.assert((prev === undefined && node === AtomicValue.load(bucket)) || AtomicValue.load(prev!.next) === node);
|
||||
if (hashCode === node.hashCode && key === node.key) {
|
||||
const currentValue = node.value;
|
||||
// We've matched the key, now match the value.
|
||||
if (expectedValue === NOT_EXIST) {
|
||||
// If we are inserting (`expectedValue === NOT_EXIST`) then we may not update if an entry exists.
|
||||
return currentValue;
|
||||
}
|
||||
else if (expectedValue === WILDCARD || expectedValue === EXIST || expectedValue === currentValue) {
|
||||
// We are either unconditionally setting (`expectedValue === WILDCARD`), updating/deleting
|
||||
// (`expectedValue === EXIST`), or deleting/replacing (`expectedValue === currentValue`).
|
||||
Debug.assert(expectedValue !== NOT_EXIST as unknown);
|
||||
if (replacementValue === NOT_EXIST) {
|
||||
// performing delete
|
||||
if (!prev) {
|
||||
AtomicValue.store(bucket, AtomicValue.load(node.next));
|
||||
}
|
||||
else {
|
||||
AtomicValue.store(prev.next, AtomicValue.load(node.next));
|
||||
}
|
||||
tables.countsPerLock[lockNo]--;
|
||||
}
|
||||
else {
|
||||
// performing unconditional set or update update
|
||||
if (isLockFree(replacementValue)) {
|
||||
node.value = replacementValue;
|
||||
}
|
||||
else {
|
||||
const newNode = new Node(node.key, replacementValue, hashCode, AtomicValue.load(node.next));
|
||||
if (!prev) {
|
||||
AtomicValue.store(bucket, newNode);
|
||||
}
|
||||
else {
|
||||
AtomicValue.store(prev.next, newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
prev = node;
|
||||
if (typeof key === "string") {
|
||||
collisionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// if we failed to find a matching key and we are either deleting or updating, we exit early indicating
|
||||
// there was no match.
|
||||
if (replacementValue === NOT_EXIST || expectedValue === EXIST) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// otherwise, we are inserting or setting so we can add the entry.
|
||||
const resultNode = new Node(key, replacementValue, hashCode, AtomicValue.load(bucket));
|
||||
AtomicValue.store(bucket, resultNode);
|
||||
tables.countsPerLock[lockNo]++;
|
||||
|
||||
if (tables.countsPerLock[lockNo] > self._budget) {
|
||||
resizeDesired = true;
|
||||
}
|
||||
|
||||
if (typeof key === "string" && collisionCount > 10 && tables.seed === undefined) {
|
||||
forceRehash = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (resizeDesired || forceRehash) {
|
||||
ConcurrentMap.growTable(self, tables, resizeDesired, forceRehash);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static clear<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>) {
|
||||
using _ = new ScopedLock(AtomicValue.load(self._tables).locks);
|
||||
if (ConcurrentMap.allBucketsAreEmpty(self)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tables = AtomicValue.load(self._tables);
|
||||
const buckets = new SharedArray<AtomicValue<Node<K, V> | undefined>>(getPrime(DefaultCapacity));
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
buckets[i] = new AtomicValue<Node<K, V> | undefined>(/*value*/ undefined);
|
||||
}
|
||||
|
||||
const counts = new SharedArray<number>(tables.countsPerLock.length);
|
||||
Array.prototype.fill.call(counts, 0);
|
||||
|
||||
const newTables = new Tables(buckets, tables.locks, counts, tables.seed);
|
||||
AtomicValue.store(self._tables, newTables);
|
||||
self._budget = Math.max(1, (newTables.buckets.length / newTables.locks.length) | 0);
|
||||
}
|
||||
|
||||
static * keys<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>): IterableIterator<K> {
|
||||
const buckets = AtomicValue.load(self._tables).buckets;
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
for (let node: Node<K, V> | undefined = AtomicValue.load(buckets[i]); node; node = AtomicValue.load(node.next)) {
|
||||
yield node.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static * values<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>): IterableIterator<V> {
|
||||
const buckets = AtomicValue.load(self._tables).buckets;
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
for (let node: Node<K, V> | undefined = AtomicValue.load(buckets[i]); node; node = AtomicValue.load(node.next)) {
|
||||
yield node.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static * entries<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>): IterableIterator<[K, V]> {
|
||||
const buckets = AtomicValue.load(self._tables).buckets;
|
||||
for (let i = 0; i < buckets.length; i++) {
|
||||
for (let node: Node<K, V> | undefined = AtomicValue.load(buckets[i]); node; node = AtomicValue.load(node.next)) {
|
||||
yield [node.key, node.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static growTable<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>, tables: Tables<K, V>, resizeDesired: boolean, forceRehashIfNonRandomized: boolean) {
|
||||
using _first = new UniqueLock(tables.locks[0]);
|
||||
if (tables !== AtomicValue.load(self._tables)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newLength = tables.buckets.length;
|
||||
let newSeed: number | undefined;
|
||||
if (forceRehashIfNonRandomized && tables.seed === undefined) {
|
||||
newSeed = generateHashSeed();
|
||||
}
|
||||
|
||||
if (resizeDesired) {
|
||||
if (newSeed === undefined && ConcurrentMap.getSizeNoLocks(self) < tables.buckets.length / 4) {
|
||||
self._budget *= 2;
|
||||
if (self._budget > (self._budget | 0)) {
|
||||
self._budget = MAX_INT32;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ((newLength = tables.buckets.length * 2) < 0 ||
|
||||
(newLength = getPrime(newLength)) > SHARED_ARRAY_MAX_LENGTH) {
|
||||
newLength = SHARED_ARRAY_MAX_LENGTH;
|
||||
self._budget = MAX_INT32;
|
||||
}
|
||||
}
|
||||
|
||||
let newLocks = tables.locks;
|
||||
if (self._growLockArray && tables.locks.length < MaxLockNumber) {
|
||||
newLocks = new SharedArray(tables.locks.length * 2);
|
||||
for (let i = 0; i < tables.locks.length; i++) {
|
||||
newLocks[i] = tables.locks[i];
|
||||
}
|
||||
for (let i = tables.locks.length; i < newLocks.length; i++) {
|
||||
newLocks[i] = new Mutex();
|
||||
}
|
||||
}
|
||||
|
||||
const newBuckets = new SharedArray<AtomicValue<Node<K, V> | undefined>>(newLength);
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
newBuckets[i] = new AtomicValue<Node<K, V> | undefined>(/*value*/ undefined);
|
||||
}
|
||||
|
||||
const newCountsPerLock = new SharedArray<number>(newLocks.length);
|
||||
Array.prototype.fill.call(newCountsPerLock, 0);
|
||||
|
||||
const newTables = new Tables(newBuckets, newLocks, newCountsPerLock, newSeed ?? tables.seed);
|
||||
|
||||
using _rest = new ScopedLock(Array.prototype.slice.call(tables.locks, 1));
|
||||
for (const bucket of iterateValues(tables.buckets)) {
|
||||
let current: Node<K, V> | undefined = AtomicValue.load(bucket);
|
||||
while (current) {
|
||||
let hashCode = newSeed === undefined ? current.hashCode : identityHash(current.key, newSeed);
|
||||
const next: Node<K, V> | undefined = AtomicValue.load(current.next);
|
||||
let { lockNo: newLockNo, bucket: newBucket } = ConcurrentMap.getBucketAndLock(newTables, hashCode);
|
||||
AtomicValue.store(newBucket, new Node(current.key, current.value, hashCode, AtomicValue.load(newBucket)));
|
||||
newCountsPerLock[newLockNo]++;
|
||||
Debug.assert(newCountsPerLock[newLockNo] <= MAX_INT32);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
self._budget = Math.max(1, (newBuckets.length / newLocks.length) | 0);
|
||||
AtomicValue.store(self._tables, newTables);
|
||||
}
|
||||
|
||||
private static getSizeNoLocks<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>) {
|
||||
let count = 0;
|
||||
const tables = AtomicValue.load(self._tables);
|
||||
const countsPerLock = tables.countsPerLock;
|
||||
for (let i = 0; i < countsPerLock.length; i++) {
|
||||
count += countsPerLock[i];
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static getBucket<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(tables: Tables<K, V>, hashCode: number) {
|
||||
const buckets = tables.buckets;
|
||||
const bucketNo = (hashCode >>> 0) % buckets.length;
|
||||
const bucket = buckets[bucketNo];
|
||||
return AtomicValue.load(bucket);
|
||||
}
|
||||
|
||||
private static getBucketAndLock<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(tables: Tables<K, V>, hashCode: number) {
|
||||
const buckets = tables.buckets;
|
||||
const bucketNo = (hashCode >>> 0) % buckets.length;
|
||||
const lockNo = bucketNo % tables.locks.length;
|
||||
const bucket = buckets[bucketNo];
|
||||
return { bucket, lockNo };
|
||||
}
|
||||
|
||||
private static allBucketsAreEmpty<K extends NonNullable<Shareable>, V extends NonNullable<Shareable>>(self: ConcurrentMap<K, V>) {
|
||||
return Array.prototype.some.call(AtomicValue.load(self._tables).countsPerLock, x => x !== 0);
|
||||
}
|
||||
}
|
||||
|
||||
function isLockFree(value: Shareable) {
|
||||
if (Atomics.isLockFree(8)) return true;
|
||||
if (typeof value === "number") return isFinite(value) && (value === (value >>> 0) || value === (value >> 0));
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Shared, SharedStructBase } from "../structs/sharedStruct";
|
||||
|
||||
@Shared()
|
||||
class Ring<T extends Shareable> extends SharedStructBase {
|
||||
@Shared() readonly size: number;
|
||||
@Shared() readonly mask: number;
|
||||
@Shared() readonly segment: SharedArray<T | undefined>;
|
||||
|
||||
constructor(size: number) {
|
||||
super();
|
||||
const mask = size - 1;
|
||||
if (size & mask) throw new RangeError("Must be a power of 2");
|
||||
|
||||
this.size = size;
|
||||
this.mask = mask;
|
||||
this.segment = new SharedArray(size);
|
||||
}
|
||||
|
||||
static size<T extends Shareable>(self: Ring<T>) {
|
||||
return Atomics.load(self, "size");
|
||||
}
|
||||
|
||||
static get<T extends Shareable>(self: Ring<T>, i: number) {
|
||||
return Atomics.load(self.segment, i & Atomics.load(self, "mask"));
|
||||
}
|
||||
|
||||
static put<T extends Shareable>(self: Ring<T>, i: number, value: T) {
|
||||
Atomics.store(self.segment, i & Atomics.load(self, "mask"), value);
|
||||
}
|
||||
|
||||
static grow<T extends Shareable>(self: Ring<T>, bottom: number, top: number) {
|
||||
const size = Atomics.load(self, "size");
|
||||
const newSize = (size << 1) >>> 0;
|
||||
if (newSize < size) throw new RangeError();
|
||||
const a: Ring<T> = new Ring(newSize);
|
||||
for (let i = top; i < bottom; i++) {
|
||||
Ring.put(a, i, Ring.get(self, i));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
static shrink<T extends Shareable>(self: Ring<T>, bottom: number, top: number) {
|
||||
const size = Atomics.load(self, "size");
|
||||
if (size <= 1) return self;
|
||||
const a: Ring<T> = new Ring(size >>> 1);
|
||||
for (let i = top; i < bottom; i++) {
|
||||
Ring.put(a, i, Ring.get(self, i));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
const kDequeInitialCapacity = 1 << 5;
|
||||
const kDequeTrimFactor = 3;
|
||||
|
||||
/**
|
||||
* Chase-Lev Deque based on "Dynamic Circular Work-Stealing Deque"
|
||||
* @see {@link https://dl.acm.org/doi/10.1145/1073970.1073974}
|
||||
* @template {Shareable} T
|
||||
*/
|
||||
@Shared()
|
||||
export class Deque<T extends Shareable> extends SharedStructBase {
|
||||
@Shared() readonly top: number;
|
||||
@Shared() readonly bottom: number;
|
||||
@Shared() readonly array: Ring<T>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.top = 0;
|
||||
this.bottom = 0;
|
||||
this.array = new Ring(kDequeInitialCapacity);
|
||||
}
|
||||
|
||||
static pushBottom<T extends Shareable>(self: Deque<T>, value: T) {
|
||||
const bottom = Atomics.load(self, "bottom");
|
||||
const top = Atomics.load(self, "top");
|
||||
let array = Atomics.load(self, "array");
|
||||
if (bottom - top > Ring.size(array) - 1) {
|
||||
array = Ring.grow(array, bottom, top);
|
||||
Atomics.store(self, "array", array);
|
||||
}
|
||||
Ring.put(array, bottom, value);
|
||||
Atomics.store(self, "bottom", bottom + 1);
|
||||
}
|
||||
|
||||
static popBottom<T extends Shareable>(self: Deque<T>) {
|
||||
const bottom = Atomics.load(self, "bottom") - 1;
|
||||
const array = Atomics.load(self, "array");
|
||||
Atomics.store(self, "bottom", bottom);
|
||||
const top = Atomics.load(self, "top");
|
||||
const size = bottom - top;
|
||||
if (size < 0) {
|
||||
Atomics.store(self, "bottom", top);
|
||||
return undefined;
|
||||
}
|
||||
let value = Ring.get(array, bottom);
|
||||
if (size > 0) {
|
||||
Deque.trim(self, bottom, top);
|
||||
return value;
|
||||
}
|
||||
const result = top === Atomics.compareExchange(self, "top", top, top + 1);
|
||||
Atomics.store(self, "bottom", top + 1);
|
||||
return result ? value : undefined;
|
||||
}
|
||||
|
||||
static steal<T extends Shareable>(self: Deque<T>) {
|
||||
const top = Atomics.load(self, "top");
|
||||
const bottom = Atomics.load(self, "bottom");
|
||||
const array = Atomics.load(self, "array");
|
||||
if (bottom - top <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const value = Ring.get(array, top);
|
||||
if (top !== Atomics.compareExchange(self, "top", top, top + 1)) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static trim<T extends Shareable>(self: Deque<T>, bottom: number, top: number) {
|
||||
const array = Atomics.load(self, "array");
|
||||
if (bottom - top < Ring.size(array) / kDequeTrimFactor) {
|
||||
Atomics.store(self, "array", Ring.shrink(array, bottom, top));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { hasProperty } from "../../core";
|
||||
import { Debug } from "../../debug";
|
||||
import { sys } from "../../sys";
|
||||
import { IdentifiableStruct } from "../structs/identifiableStruct";
|
||||
import { xxh32string } from "./xxhash32";
|
||||
|
||||
const defaultStringSeed = sys.stringSeed ?? generateHashSeed();
|
||||
|
||||
/** @internal */
|
||||
export function generateHashSeed() {
|
||||
return Math.floor(Math.random() * 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a hashcode for a value that is consistent across multiple threads.
|
||||
* @internal
|
||||
*/
|
||||
export function identityHash(value: Shareable, seed?: number): number {
|
||||
if (value === undefined || value === null) { // eslint-disable-line no-null/no-null -- necessary comparison
|
||||
return 0;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
return value >> 0;
|
||||
case "boolean":
|
||||
return value ? 1 : 0;
|
||||
case "string":
|
||||
return xxh32string(value, seed ?? defaultStringSeed) >> 0;
|
||||
case "object":
|
||||
if (hasProperty(value, "__hash__")) {
|
||||
return (value as IdentifiableStruct).__hash__ >> 0;
|
||||
}
|
||||
return 0;
|
||||
default:
|
||||
Debug.assertNever(value);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,6 @@
|
||||
import { hasProperty } from "../../core";
|
||||
import { Debug } from "../../debug";
|
||||
import { sys } from "../../sys";
|
||||
import { IdentifiableStruct } from "../structs/identifiableStruct";
|
||||
import { Shared, SharedStructBase } from "../structs/sharedStruct";
|
||||
import { xxh32string } from "./xxhash32";
|
||||
import { identityHash } from "./hash";
|
||||
|
||||
const seed = sys.stringSeed ?? Math.floor(Math.random() * 0xffffffff) >>> 0;
|
||||
|
||||
function hash(value: Shareable) {
|
||||
if (value === undefined || value === null) { // eslint-disable-line no-null/no-null -- necessary comparison
|
||||
return 0;
|
||||
}
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
return value >> 0;
|
||||
case "boolean":
|
||||
return value ? 1 : 0;
|
||||
case "string":
|
||||
return xxh32string(value, seed);
|
||||
case "object":
|
||||
if (hasProperty(value, "__hash__")) {
|
||||
return (value as IdentifiableStruct).__hash__ >> 0;
|
||||
}
|
||||
return 0;
|
||||
default:
|
||||
Debug.assertNever(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Portions of the following are derived from esfx and .NET Core. See ThirdPartyNoticeText.txt in the root of this
|
||||
// repository for their respective license notices.
|
||||
@@ -80,7 +54,8 @@ function isPrime(candidate: number) {
|
||||
return candidate === 2;
|
||||
}
|
||||
|
||||
function getPrime(min: number) {
|
||||
/** @internal */
|
||||
export function getPrime(min: number) {
|
||||
if (min < 0) throw new RangeError();
|
||||
for (const prime of primes) {
|
||||
if (prime >= min) return prime;
|
||||
@@ -171,7 +146,7 @@ export class HashData<K extends Shareable, V extends Shareable> extends SharedSt
|
||||
}
|
||||
|
||||
static findEntryIndex<K extends Shareable, V extends Shareable>(hashData: HashData<K, V>, key: K) {
|
||||
const hashCode = hash(key) & MAX_INT32;
|
||||
const hashCode = identityHash(key);
|
||||
// Value in _buckets is 1-based
|
||||
let i = hashData.buckets[hashCode % hashData.buckets.length] - 1;
|
||||
const length = hashData.entries.length;
|
||||
@@ -191,7 +166,7 @@ export class HashData<K extends Shareable, V extends Shareable> extends SharedSt
|
||||
}
|
||||
|
||||
static insertEntry<K extends Shareable, V extends Shareable>(hashData: HashData<K, V>, key: K, value: V) {
|
||||
const hashCode = hash(key) & MAX_INT32;
|
||||
const hashCode = identityHash(key) & MAX_INT32;
|
||||
let bucket = hashCode % hashData.buckets.length;
|
||||
// Value in _buckets is 1-based
|
||||
let i = hashData.buckets[bucket] - 1;
|
||||
@@ -236,7 +211,7 @@ export class HashData<K extends Shareable, V extends Shareable> extends SharedSt
|
||||
}
|
||||
|
||||
static deleteEntry<K extends Shareable, V extends Shareable>(hashData: HashData<K, V>, key: K) {
|
||||
const hashCode = hash(key) & MAX_INT32;
|
||||
const hashCode = identityHash(key) & MAX_INT32;
|
||||
const bucket = hashCode % hashData.buckets.length;
|
||||
let last = -1;
|
||||
let entry: HashEntry<K, V> | undefined;
|
||||
|
||||
+403
-604
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,13 +1,12 @@
|
||||
import { TransformFlags } from "../types";
|
||||
import type { SharedNodeBase } from "./sharedNode";
|
||||
import type { SharedNode } from "./sharedNode";
|
||||
import { Identifiable } from "./structs/identifiableStruct";
|
||||
import { isShareableNonPrimitive } from "./structs/shareable";
|
||||
import { Shared, SharedStructBase } from "./structs/sharedStruct";
|
||||
import { isTaggedStruct, Tag, Tagged } from "./structs/taggedStruct";
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
export class SharedNodeArray<T extends SharedNodeBase> extends Identifiable(Tagged(SharedStructBase, Tag.NodeArray)) {
|
||||
export class SharedNodeArray<T extends SharedNode> extends Identifiable(Tagged(SharedStructBase, Tag.NodeArray)) {
|
||||
@Shared() items!: SharedArray<T>;
|
||||
@Shared() pos = -1;
|
||||
@Shared() end = -1;
|
||||
@@ -15,15 +14,13 @@ export class SharedNodeArray<T extends SharedNodeBase> extends Identifiable(Tagg
|
||||
@Shared() transformFlags = TransformFlags.None;
|
||||
@Shared() isMissingList = false;
|
||||
|
||||
static * values<T extends SharedNodeBase>(self: SharedNodeArray<T>): IterableIterator<T> {
|
||||
static * values<T extends SharedNode>(self: SharedNodeArray<T>): IterableIterator<T> {
|
||||
for (let i = 0; i < self.items.length; i++) {
|
||||
yield self.items[i];
|
||||
}
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](value: unknown): value is SharedNodeArray<SharedNodeBase> {
|
||||
return isShareableNonPrimitive(value) &&
|
||||
isTaggedStruct(value) &&
|
||||
value.__tag__ === Tag.NodeArray;
|
||||
static [Symbol.hasInstance](value: unknown): value is SharedNodeArray<SharedNode> {
|
||||
return isTaggedStruct(value, Tag.NodeArray);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Identifier, Node, NodeArray, PrivateIdentifier, SourceFile, SyntaxKind, Token } from "../types";
|
||||
import { ObjectAllocator, objectAllocator } from "../utilities";
|
||||
import { getSharedConstructorForKind, SharedNodeBase } from "./sharedNode";
|
||||
import { getSharedConstructorForKind } from "./sharedNode";
|
||||
import { SharedNodeArray } from "./sharedNodeArray";
|
||||
|
||||
function NodeArray(items: readonly any[], hasTrailingComma?: boolean) {
|
||||
@@ -15,7 +15,7 @@ function NodeArray(items: readonly any[], hasTrailingComma?: boolean) {
|
||||
|
||||
function Node(kind: SyntaxKind, pos: number, end: number) {
|
||||
const SharedNode = getSharedConstructorForKind(kind);
|
||||
const node = new SharedNode() as SharedNodeBase;
|
||||
const node = new SharedNode();
|
||||
node.kind = kind;
|
||||
node.pos = pos;
|
||||
node.end = end;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Condition } from "../threading/condition";
|
||||
import { Mutex } from "../threading/mutex";
|
||||
import { SharedMutex } from "../threading/sharedMutex";
|
||||
import { ResolutionMode, ScriptTarget } from "../types";
|
||||
import { SharedMap } from "./collections/sharedMap";
|
||||
import { ConcurrentMap } from "./collections/concurrentMap";
|
||||
import { SharedSourceFile } from "./sharedNode";
|
||||
import { Shared, SharedStructBase } from "./structs/sharedStruct";
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
export class SharedParserState extends SharedStructBase {
|
||||
@Shared() sharedMutex = new SharedMutex();
|
||||
@Shared() files = new SharedMap<string, SharedSourceFileEntry>();
|
||||
// @Shared() sharedMutex = new SharedMutex();
|
||||
// @Shared() files = new SharedMap<string, SharedSourceFileEntry>();
|
||||
@Shared() files = new ConcurrentMap<string, SharedSourceFileEntry>();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
||||
@@ -44,7 +44,9 @@ declare global {
|
||||
|
||||
interface SharedStructTypeConstructor {
|
||||
new <T extends SharedStruct & { readonly [P in keyof T]: Shareable }>(fields: readonly (keyof T & string)[]): SharedStructType<T>;
|
||||
new (fields: readonly string[]): SharedStructType<SharedStruct>;
|
||||
new <K extends string>(fields: readonly K[]): SharedStructType<SharedStruct> & Record<K, Shareable>;
|
||||
<T extends SharedStruct & { readonly [P in keyof T]: Shareable }>(fields: readonly (keyof T & string)[]): SharedStructType<T>;
|
||||
<K extends string>(fields: readonly K[]): SharedStructType<SharedStruct> & Record<K, Shareable>;
|
||||
isSharedStruct(value: unknown): value is SharedStruct;
|
||||
}
|
||||
|
||||
@@ -56,6 +58,11 @@ declare global {
|
||||
readonly length: number;
|
||||
}
|
||||
|
||||
type SharedTuple<T extends Shareable[]> =
|
||||
& SharedArray<T[number]>
|
||||
& { readonly length: T["length"] }
|
||||
& { [P in keyof T as P extends `${bigint}` ? P : never]: T[P] };
|
||||
|
||||
interface ReadonlySharedArray<T extends Shareable> extends SharedArray<T> {
|
||||
readonly [index: number]: T;
|
||||
}
|
||||
@@ -120,5 +127,13 @@ declare global {
|
||||
interface Atomics {
|
||||
readonly Mutex: Atomics.MutexConstructor;
|
||||
readonly Condition: Atomics.ConditionConstructor;
|
||||
compareExchange<T extends Shareable>(sharedArray: SharedArray<T>, index: number, expectedValue: T, replacementValue: T): T;
|
||||
compareExchange<T extends SharedStruct, K extends keyof T & string>(sharedStruct: T, key: K, expectedValue: T[K], replacementValue: T[K]): T[K];
|
||||
exchange<T extends Shareable>(sharedArray: SharedArray<T>, index: number, value: T): T;
|
||||
exchange<T extends SharedStruct, K extends keyof T & string>(sharedStruct: T, key: K, value: T[K]): T[K];
|
||||
load<T extends Shareable>(sharedArray: SharedArray<T>, index: number): T;
|
||||
load<T extends SharedStruct, K extends keyof T & string>(sharedStruct: T, key: K): T[K];
|
||||
store<T extends Shareable>(sharedArray: SharedArray<T>, index: number, value: T): T;
|
||||
store<T extends SharedStruct, K extends keyof T & string, V extends T[K]>(sharedStruct: T, key: K, value: V): V;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ export const enum Tag {
|
||||
Condition,
|
||||
ManualResetEvent,
|
||||
CountdownEvent,
|
||||
Thread,
|
||||
Map,
|
||||
Set,
|
||||
ResizableArray,
|
||||
ConcurrentMap,
|
||||
Semaphore,
|
||||
NodeArray,
|
||||
Node,
|
||||
Symbol,
|
||||
@@ -53,6 +56,10 @@ export function Tagged<F extends abstract new (...args: any) => SharedStruct, TT
|
||||
abstract class TaggedStruct extends base {
|
||||
static readonly __tag__ = tag;
|
||||
@Shared() readonly __tag__ = tag;
|
||||
|
||||
static [Symbol.hasInstance](value: unknown): boolean {
|
||||
return isTaggedStruct(value, tag);
|
||||
}
|
||||
}
|
||||
return TaggedStruct as Tagged<F, TTag>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Debug } from "../../debug";
|
||||
|
||||
/**
|
||||
* Used in a `declare` instance field of a struct type to override the type we use for a wrapper object generated for
|
||||
* the struct. We do this because we cannot properly infer correct generic instantiations from generic static methods.
|
||||
* This declaration intentionally does not produce an actual value as it should only be used in an ambient context.
|
||||
* @internal
|
||||
*/
|
||||
export declare const StructWrapperTypes: unique symbol;
|
||||
|
||||
/** @internal */
|
||||
export type StructWrapper<TInstance extends SharedStruct, TStatics extends object> =
|
||||
TInstance extends { [StructWrapperTypes]: infer T extends [any, any][] } ?
|
||||
T[number] extends [TStatics, infer U] ? U :
|
||||
SynthesizedStructWrapper<TInstance, TStatics> :
|
||||
SynthesizedStructWrapper<TInstance, TStatics>;
|
||||
|
||||
type SynthesizedStructInstanceFields<TInstance extends SharedStruct> = {
|
||||
[P in keyof TInstance & string as Exclude<P, "__tag__" | "__hash__">]: TInstance[P];
|
||||
};
|
||||
|
||||
type SynthesizedStructInstanceMethods<TInstance extends SharedStruct, TStatics extends object> = {
|
||||
[P in keyof TStatics & string as TStatics[P] extends (self: TInstance, ...args: any) => any ? Exclude<P, `_${string}`> : never]:
|
||||
TStatics[P] extends (self: TInstance, ...args: infer A) => infer R ? (...args: A) => R : never;
|
||||
};
|
||||
|
||||
type SynthesizedStructWrapper<TInstance extends SharedStruct, TStatics extends object> =
|
||||
& SynthesizedStructInstanceFields<TInstance>
|
||||
& SynthesizedStructInstanceMethods<TInstance, TStatics>
|
||||
;
|
||||
|
||||
/**
|
||||
* Generates a simple, shallow proxy for a shared struct that treats static methods as instance methods with the first
|
||||
* parameter bound to the instance.
|
||||
* @internal
|
||||
*/
|
||||
export function wrapStruct<TInstance extends SharedStruct, TStatics extends object>(instance: TInstance, statics: TStatics): StructWrapper<TInstance, TStatics> {
|
||||
const wrapper = {} as any;
|
||||
for (const key of Reflect.ownKeys(instance) as Iterable<keyof TInstance>) {
|
||||
if (typeof key !== "string") continue;
|
||||
if (key === "__tag__" || key == "__hash__") continue;
|
||||
Object.defineProperty(wrapper, key, {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => instance[key],
|
||||
set: (value: TInstance[typeof key]) => instance[key] = value
|
||||
});
|
||||
}
|
||||
|
||||
for (const key of Reflect.ownKeys(statics) as Iterable<keyof TStatics>) {
|
||||
if (typeof key !== "string") continue;
|
||||
if (key.startsWith("_")) continue;
|
||||
const value = statics[key];
|
||||
if (typeof value !== "function") continue;
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) Debug.fail(`static method name '${key}' conflicts with same named instance field`);
|
||||
Object.defineProperty(wrapper, key, {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: value.bind(/*thisArg*/ undefined, instance)
|
||||
});
|
||||
}
|
||||
|
||||
return wrapper as StructWrapper<TInstance, TStatics>;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
/// <reference lib="esnext.disposable" />
|
||||
export {};
|
||||
|
||||
import { Debug } from "./debug";
|
||||
|
||||
export { };
|
||||
|
||||
// Shim `Symbol.dispose` so that we can can use `using`.
|
||||
if (!Symbol.dispose) {
|
||||
@@ -10,3 +13,105 @@ if (!Symbol.dispose) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createSuppressedError(error: unknown, suppressed: unknown) {
|
||||
if (typeof SuppressedError === "function") {
|
||||
return Debug.captureStackTrace(new SuppressedError(error, suppressed), createSuppressedError);
|
||||
}
|
||||
|
||||
const e = new Error("An error suppression occurred.") as SuppressedError;
|
||||
e.error = error;
|
||||
e.suppressed = suppressed;
|
||||
e.name = "SuppressedError";
|
||||
return Debug.captureStackTrace(e, createSuppressedError);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createDisposableStack() {
|
||||
return new disposableStackConstructor();
|
||||
}
|
||||
|
||||
const disposableStackConstructor = typeof DisposableStack === "function" ? DisposableStack : createDisposableStackShim();
|
||||
|
||||
function createDisposableStackShim(): new () => DisposableStack {
|
||||
class DisposableStack implements globalThis.DisposableStack {
|
||||
private _stack: { resource: unknown, dispose: (this: unknown) => void }[] = [];
|
||||
private _disposed = false;
|
||||
|
||||
get disposed(): boolean {
|
||||
return this._disposed;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposed = true;
|
||||
|
||||
let error: unknown;
|
||||
let hasError = false;
|
||||
let entry;
|
||||
while (entry = this._stack.pop()) {
|
||||
try {
|
||||
const { resource, dispose } = entry;
|
||||
dispose.call(resource);
|
||||
}
|
||||
catch (e) {
|
||||
error = hasError ? createSuppressedError(e, error) : e;
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
use<T extends Disposable | null | undefined>(value: T): T {
|
||||
if (this._disposed) throw new ReferenceError("Object is disposed");
|
||||
if (value === undefined) return value;
|
||||
Debug.assert(typeof value === "object");
|
||||
if (!value) return value;
|
||||
const dispose = value[Symbol.dispose];
|
||||
Debug.assert(typeof dispose === "function");
|
||||
this._stack.push({ resource: value, dispose });
|
||||
return value;
|
||||
}
|
||||
|
||||
adopt<T>(value: T, onDispose: (value: T) => void): T {
|
||||
if (this._disposed) throw new ReferenceError("Object is disposed");
|
||||
Debug.assert(typeof onDispose === "function");
|
||||
this._stack.push({ resource: undefined, dispose: () => { onDispose(value); } });
|
||||
return value;
|
||||
}
|
||||
|
||||
defer(onDispose: () => void): void {
|
||||
if (this._disposed) throw new ReferenceError("Object is disposed");
|
||||
Debug.assert(typeof onDispose === "function");
|
||||
this._stack.push({ resource: undefined, dispose: onDispose });
|
||||
}
|
||||
|
||||
move(): globalThis.DisposableStack {
|
||||
if (this._disposed) throw new ReferenceError("Object is disposed");
|
||||
const stack = new DisposableStack();
|
||||
stack._stack = this._stack;
|
||||
this._stack = [];
|
||||
this._disposed = true;
|
||||
return stack;
|
||||
}
|
||||
|
||||
[Symbol.dispose](): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
[Symbol.toStringTag]!: string;
|
||||
|
||||
static {
|
||||
this.prototype[Symbol.dispose] = this.prototype.dispose;
|
||||
Object.defineProperty(this.prototype, Symbol.toStringTag, { configurable: true, writable: true, value: "DisposableStack" });
|
||||
}
|
||||
}
|
||||
return DisposableStack;
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ export class Condition extends Tagged(SharedStructBase, Tag.Condition) {
|
||||
* @param self The condition to wait for.
|
||||
* @param lock A {@link UniqueLock} taken for a {@link Mutex}.
|
||||
* @param timeout The number of milliseconds to wait before returning.
|
||||
* @returns `"no-timeout"` if the condition was notified before the timeout elapsed, or `"timeout"` to indicate the
|
||||
* @returns `"ok"` if the condition was notified before the timeout elapsed, or `"timed-out"` to indicate the
|
||||
* timeout elapsed before the condition was notified.
|
||||
*/
|
||||
static waitFor(self: Condition, lock: UniqueLock<Mutex>, timeout: number): "no-timeout" | "timeout";
|
||||
static waitFor(self: Condition, lock: UniqueLock<Mutex>, timeout: number): "ok" | "timed-out";
|
||||
/**
|
||||
* Waits until a Condition is signaled via a call to {@link notify|`Condition.notify`}, or until a timeout period
|
||||
* has elapsed.
|
||||
@@ -40,7 +40,7 @@ export class Condition extends Tagged(SharedStructBase, Tag.Condition) {
|
||||
* `false` to indicate that the timeout elapsed before the condition was ready.
|
||||
*/
|
||||
static waitFor(self: Condition, lock: UniqueLock<Mutex>, timeout: number, stopWaiting: () => boolean): boolean;
|
||||
static waitFor(self: Condition, lock: UniqueLock<Mutex>, timeout: number, stopWaiting?: () => boolean): "no-timeout" | "timeout" | boolean {
|
||||
static waitFor(self: Condition, lock: UniqueLock<Mutex>, timeout: number, stopWaiting?: () => boolean): "ok" | "timed-out" | boolean {
|
||||
return Condition.waitUntil(self, lock, Date.now() + timeout, stopWaiting!);
|
||||
}
|
||||
|
||||
@@ -49,10 +49,10 @@ export class Condition extends Tagged(SharedStructBase, Tag.Condition) {
|
||||
* @param self The condition to wait for.
|
||||
* @param lock A {@link UniqueLock} taken for a {@link Mutex}.
|
||||
* @param absoluteTimeout The number of milliseconds since the UNIX epoch.
|
||||
* @returns `"no-timeout"` if the condition was notified before the timeout elapsed, or `"timeout"` to indicate the
|
||||
* @returns `"ok"` if the condition was notified before the timeout elapsed, or `"timed-out"` to indicate the
|
||||
* timeout elapsed before the condition was notified.
|
||||
*/
|
||||
static waitUntil(self: Condition, lock: UniqueLock<Mutex>, absoluteTimeout: number): "no-timeout" | "timeout";
|
||||
static waitUntil(self: Condition, lock: UniqueLock<Mutex>, absoluteTimeout: number): "ok" | "timed-out";
|
||||
/**
|
||||
* Waits until a Condition is signaled via a call to {@link notify|`Condition.notify`}, or until a specific time has been reached.
|
||||
* @param self The condition to wait for.
|
||||
@@ -63,7 +63,7 @@ export class Condition extends Tagged(SharedStructBase, Tag.Condition) {
|
||||
* `false` to indicate that the timeout elapsed before the condition was ready.
|
||||
*/
|
||||
static waitUntil(self: Condition, lock: UniqueLock<Mutex>, absoluteTimeout: number, stopWaiting: () => boolean): boolean;
|
||||
static waitUntil(self: Condition, lock: UniqueLock<Mutex>, absoluteTimeout: number, stopWaiting?: () => boolean): "no-timeout" | "timeout" | boolean {
|
||||
static waitUntil(self: Condition, lock: UniqueLock<Mutex>, absoluteTimeout: number, stopWaiting?: () => boolean): "ok" | "timed-out" | boolean {
|
||||
const mutex = lock.mutex;
|
||||
Debug.assert(mutex);
|
||||
Debug.assert(mutex["_locked"]); // eslint-disable-line dot-notation -- declared `private`
|
||||
@@ -75,12 +75,12 @@ export class Condition extends Tagged(SharedStructBase, Tag.Condition) {
|
||||
Atomics.Condition.notify(nativeCondition);
|
||||
try {
|
||||
const remainingTimeout = isFinite(absoluteTimeout) ? Date.now() - absoluteTimeout : undefined;
|
||||
const result = Atomics.Condition.wait(self._condition, nativeMutex, remainingTimeout) ? "no-timeout" : "timeout";
|
||||
if (result === "timeout") {
|
||||
const result = Atomics.Condition.wait(self._condition, nativeMutex, remainingTimeout) ? "ok" : "timed-out";
|
||||
if (result === "timed-out") {
|
||||
return stopWaiting ? stopWaiting() : result;
|
||||
}
|
||||
if (!stopWaiting) {
|
||||
return "no-timeout";
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -99,7 +99,7 @@ export class CountdownEvent extends Tagged(SharedStructBase, Tag.CountdownEvent)
|
||||
|
||||
using lck = new UniqueLock(self._mutex);
|
||||
if (timeout !== undefined) {
|
||||
return Condition.waitFor(self._condition, lck, timeout) !== "timeout";
|
||||
return Condition.waitFor(self._condition, lck, timeout) !== "timed-out";
|
||||
}
|
||||
else {
|
||||
Condition.wait(self._condition, lck);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/** @internal */
|
||||
export interface BasicLockable {
|
||||
export interface Lockable {
|
||||
tryLock(): boolean;
|
||||
lock(): void;
|
||||
unlock(): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface Lockable extends BasicLockable {
|
||||
tryLock(): boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Shared, SharedStructBase } from "../sharing/structs/sharedStruct";
|
||||
import { Tag, Tagged } from "../sharing/structs/taggedStruct";
|
||||
import { isTaggedStruct, Tag, Tagged } from "../sharing/structs/taggedStruct";
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
@@ -38,4 +38,8 @@ export class ManualResetEvent extends Tagged(SharedStructBase, Tag.ManualResetEv
|
||||
static wait(self: ManualResetEvent, timeout?: number) {
|
||||
return Atomics.Mutex.lock(self.mutex, () => Atomics.Condition.wait(self.condition, self.mutex, timeout));
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](value: unknown): value is ManualResetEvent {
|
||||
return isTaggedStruct(value, Tag.ManualResetEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { hasProperty } from "../core";
|
||||
import { Debug } from "../debug";
|
||||
import { Identifiable } from "../sharing/structs/identifiableStruct";
|
||||
import { Shared, SharedStructBase } from "../sharing/structs/sharedStruct";
|
||||
import { Tag, Tagged, TaggedStruct } from "../sharing/structs/taggedStruct";
|
||||
import { isTaggedStruct, Tag, Tagged } from "../sharing/structs/taggedStruct";
|
||||
import { Lockable } from "./lockable";
|
||||
|
||||
let tryLock: (self: Mutex, cacheKey?: object) => boolean;
|
||||
@@ -120,9 +119,7 @@ export class Mutex extends Identifiable(Tagged(SharedStructBase, Tag.Mutex)) {
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](value: unknown): value is Mutex {
|
||||
return typeof value === "object" && value !== null && // eslint-disable-line no-null/no-null -- necessary for comparison
|
||||
hasProperty(value, "__tag__") &&
|
||||
(value as TaggedStruct<Tag>).__tag__ === Tag.Mutex;
|
||||
return isTaggedStruct(value, Tag.Mutex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,19 +132,19 @@ class LockableMutex implements Lockable {
|
||||
}
|
||||
|
||||
tryLock(): boolean {
|
||||
Debug.assert(!this._ownsLock);
|
||||
Debug.assert(!this._ownsLock, "cannot take a lock you aleady own.");
|
||||
this._ownsLock = tryLock(this._mutex, this);
|
||||
return this._ownsLock;
|
||||
}
|
||||
|
||||
lock(): void {
|
||||
Debug.assert(!this._ownsLock);
|
||||
Debug.assert(!this._ownsLock, "cannot take a lock you aleady own.");
|
||||
lock(this._mutex, this);
|
||||
this._ownsLock = true;
|
||||
}
|
||||
|
||||
unlock(): void {
|
||||
Debug.assert(this._ownsLock);
|
||||
Debug.assert(this._ownsLock, "cannot release a lock you do not own.");
|
||||
unlock(this._mutex, this);
|
||||
this._ownsLock = false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Debug } from "../debug";
|
||||
import "../symbolDisposeShim";
|
||||
import { createSuppressedError } from "../symbolDisposeShim";
|
||||
|
||||
import { Lockable } from "./lockable";
|
||||
import { Mutex } from "./mutex";
|
||||
@@ -9,13 +9,13 @@ import { SharedMutex } from "./sharedMutex";
|
||||
export class ScopedLock {
|
||||
private _mutexes: readonly Lockable[] | undefined;
|
||||
|
||||
constructor(mutexes: Iterable<Lockable | Mutex | SharedMutex>) {
|
||||
constructor(mutexes: ArrayLike<Mutex | SharedMutex> | Iterable<Lockable | Mutex | SharedMutex>) {
|
||||
const array: Lockable[] = [];
|
||||
for (const mutex of mutexes) {
|
||||
for (const mutex of Array.from(mutexes)) {
|
||||
array.push(
|
||||
mutex instanceof Mutex ? Mutex.asLockable(mutex) :
|
||||
mutex instanceof SharedMutex ? SharedMutex.asLockable(mutex) :
|
||||
mutex);
|
||||
mutex instanceof SharedMutex ? SharedMutex.asLockable(mutex) :
|
||||
mutex);
|
||||
}
|
||||
|
||||
let remaining = array.length;
|
||||
@@ -36,30 +36,28 @@ export class ScopedLock {
|
||||
// always wait for the first lock
|
||||
lockable.lock();
|
||||
}
|
||||
else {
|
||||
if (lockable.tryLock()) {
|
||||
// this lock was taken. move to the next lock
|
||||
index = (index + 1) % array.length;
|
||||
remaining--;
|
||||
}
|
||||
else {
|
||||
// if we fail to take a lock, unlock each lock taken so far so that we start over at the current
|
||||
// index.
|
||||
let i = (index + array.length - 1) % array.length;
|
||||
while (remaining < array.length) {
|
||||
// always unlock all locks taken, even if one unlock fails for some reason.
|
||||
try {
|
||||
array[i].unlock();
|
||||
}
|
||||
catch (e) {
|
||||
error = error ? createSuppressedError(e, error) : e;
|
||||
hasError = true;
|
||||
}
|
||||
i = (index + array.length - 1) % array.length;
|
||||
remaining++;
|
||||
else if (!lockable.tryLock()) {
|
||||
// if we fail to take a lock, unlock each lock taken so far so that we start over at the current
|
||||
// index.
|
||||
let i = (index + array.length - 1) % array.length;
|
||||
while (remaining < array.length) {
|
||||
// always unlock all locks taken, even if one unlock fails for some reason.
|
||||
try {
|
||||
array[i].unlock();
|
||||
}
|
||||
catch (e) {
|
||||
error = error ? createSuppressedError(e, error) : e;
|
||||
hasError = true;
|
||||
}
|
||||
i = (index + array.length - 1) % array.length;
|
||||
remaining++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// this lock was taken. move to the next lock
|
||||
index = (index + 1) % array.length;
|
||||
remaining--;
|
||||
}
|
||||
catch (e) {
|
||||
error = error ? createSuppressedError(e, error) : e;
|
||||
@@ -94,15 +92,3 @@ export class ScopedLock {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createSuppressedError(error: unknown, suppressed: unknown) {
|
||||
if (typeof SuppressedError === "function") {
|
||||
return Debug.captureStackTrace(new SuppressedError(error, suppressed), createSuppressedError);
|
||||
}
|
||||
|
||||
const e = new Error("An error suppression occurred.") as SuppressedError;
|
||||
e.error = error;
|
||||
e.suppressed = suppressed;
|
||||
e.name = "SuppressedError";
|
||||
return Debug.captureStackTrace(e, createSuppressedError);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Condition, Debug, Mutex, Shared, SharedStructBase, Tag, Tagged, UniqueLock } from "../_namespaces/ts";
|
||||
import { AtomicValue } from "../sharing/atomicValue";
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
export class Semaphore extends Tagged(SharedStructBase, Tag.Semaphore) {
|
||||
@Shared() private readonly _maxCount: number;
|
||||
@Shared() private readonly _currentCount: AtomicValue<number>;
|
||||
@Shared() private readonly _waitCount = new AtomicValue<number>(0);
|
||||
@Shared() private readonly _mutex = new Mutex();
|
||||
@Shared() private readonly _condition = new Condition();
|
||||
|
||||
constructor(initialCount: number, maxCount: number = 2 ** 31 - 1) {
|
||||
Debug.assert(initialCount === (initialCount | 0) && initialCount >= 0);
|
||||
Debug.assert(maxCount >= 0 && maxCount >= initialCount);
|
||||
super();
|
||||
this._maxCount = maxCount;
|
||||
this._currentCount = new AtomicValue(initialCount);
|
||||
}
|
||||
|
||||
static count(self: Semaphore) {
|
||||
return AtomicValue.load(self._currentCount);
|
||||
}
|
||||
|
||||
static tryAcquire(self: Semaphore, timeout?: number) {
|
||||
if (timeout !== undefined) {
|
||||
if (isNaN(timeout) || isFinite(timeout)) timeout |= 0;
|
||||
if (timeout < 0) timeout = 0;
|
||||
}
|
||||
|
||||
// first, try to acquire a count lock-free
|
||||
let count = AtomicValue.load(self._currentCount);
|
||||
if (count > 0 && count === (count = AtomicValue.compareExchange(self._currentCount, count, count - 1))) {
|
||||
return true;
|
||||
}
|
||||
if (timeout === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
let remaining: number;
|
||||
|
||||
// if that fails, take a lock and wait for a spot to open up
|
||||
using lck = new UniqueLock(self._mutex);
|
||||
using _waiter = waitScope(self._waitCount);
|
||||
do {
|
||||
remaining = timeout - (Date.now() - start);
|
||||
if (count <= 0) {
|
||||
if (Condition.waitFor(self._condition, lck, remaining) === "timed-out") {
|
||||
return false;
|
||||
}
|
||||
count = AtomicValue.load(self._currentCount);
|
||||
}
|
||||
else if (count === (count = AtomicValue.compareExchange(self._currentCount, count, count - 1))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
while (remaining >= 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
static acquire(self: Semaphore) {
|
||||
Semaphore.tryAcquire(self, Infinity);
|
||||
}
|
||||
|
||||
static release(self: Semaphore, count = 1) {
|
||||
let previousCount = AtomicValue.load(self._currentCount);
|
||||
let nextCount: number;
|
||||
let waitCount: number;
|
||||
do {
|
||||
Debug.assert(self._maxCount - previousCount >= count);
|
||||
nextCount = previousCount + count;
|
||||
waitCount = AtomicValue.load(self._waitCount);
|
||||
}
|
||||
while (previousCount !== (previousCount = AtomicValue.compareExchange(self._currentCount, previousCount, nextCount)));
|
||||
if (nextCount === 1 && waitCount === 1) {
|
||||
Condition.notify(self._condition, 1);
|
||||
}
|
||||
else if (waitCount > 1) {
|
||||
Condition.notify(self._condition);
|
||||
}
|
||||
return previousCount;
|
||||
}
|
||||
}
|
||||
|
||||
function waitScope(count: AtomicValue<number>) {
|
||||
AtomicValue.increment(count);
|
||||
return {
|
||||
[Symbol.dispose]() {
|
||||
AtomicValue.decrement(count);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// import { Shared, SharedStructBase } from "../_namespaces/ts";
|
||||
// import { AtomicValue } from "../sharing/atomicValue";
|
||||
|
||||
// @Shared()
|
||||
// class DataStruct<T extends Shareable> extends SharedStructBase {
|
||||
// @Shared() internalCounter = new AtomicValue<number>(0);
|
||||
// @Shared() object: T;
|
||||
|
||||
// constructor(object: T) {
|
||||
// super();
|
||||
// this.object = object;
|
||||
// }
|
||||
|
||||
// static releaseRef<T extends Shareable>(self: DataStruct<T>) {
|
||||
// if (AtomicValue.add(self.internalCounter, 1) == -1) {
|
||||
// DataStruct.destroy(self);
|
||||
// }
|
||||
// }
|
||||
|
||||
// static destroy<T extends Shareable>(self: DataStruct<T>) {
|
||||
// self.internalCounter = undefined!;
|
||||
// self.object = undefined!;
|
||||
// }
|
||||
// }
|
||||
|
||||
// @Shared()
|
||||
// class DataPtrStruct<T extends Shareable> extends SharedStructBase {
|
||||
// @Shared() externalCounter: number;
|
||||
// @Shared() ptr: DataStruct<T> | undefined;
|
||||
// constructor(ptr: DataStruct<T> | undefined, externalCounter = 0) {
|
||||
// super();
|
||||
// this.ptr = ptr;
|
||||
// this.externalCounter = externalCounter;
|
||||
// }
|
||||
// }
|
||||
|
||||
// class DataGuard<T extends Shareable> {
|
||||
// private _ptr: DataStruct<T> | undefined;
|
||||
|
||||
// constructor(ptr: DataStruct<T> | undefined) {
|
||||
// this._ptr = ptr;
|
||||
// }
|
||||
|
||||
// get isValid() { return !!this._ptr; }
|
||||
|
||||
// get object() {
|
||||
// if (!this._ptr) throw new ReferenceError("Object is disposed");
|
||||
// return this._ptr.object;
|
||||
// }
|
||||
|
||||
// replaceWith(other: DataGuard<T>) {
|
||||
// if (this._ptr) {
|
||||
// DataStruct.releaseRef(this._ptr);
|
||||
// }
|
||||
// this._ptr = other._ptr;
|
||||
// other._ptr = undefined;
|
||||
// return this;
|
||||
// }
|
||||
|
||||
// move() {
|
||||
// const obj = new DataGuard<T>(this._ptr);
|
||||
// this._ptr = undefined;
|
||||
// return obj;
|
||||
// }
|
||||
|
||||
// [Symbol.dispose]() {
|
||||
// if (this._ptr) {
|
||||
// DataStruct.releaseRef(this._ptr);
|
||||
// this._ptr = undefined;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// @Shared()
|
||||
// class SharedGuard<T extends Shareable> extends SharedStructBase {
|
||||
// @Shared() private data_ptr = new AtomicValue<DataPtrStruct<T>>(new DataPtrStruct<T>(undefined));
|
||||
|
||||
// private static release<T extends Shareable>(old_data_ptr: DataPtrStruct<T>) {
|
||||
// if (!old_data_ptr.ptr) return;
|
||||
// const external = old_data_ptr.externalCounter;
|
||||
// if (AtomicValue.sub(old_data_ptr.ptr.internalCounter, external) === external - 1) {
|
||||
// DataStruct.destroy(old_data_ptr.ptr);
|
||||
// }
|
||||
// else {
|
||||
// DataStruct.releaseRef(old_data_ptr.ptr);
|
||||
// }
|
||||
// }
|
||||
|
||||
// static destroy<T extends Shareable>(self: SharedGuard<T>) {
|
||||
// const old_data_ptr = AtomicValue.load(self.data_ptr);
|
||||
// SharedGuard.release(old_data_ptr);
|
||||
// }
|
||||
|
||||
// static acquire<T extends Shareable>(self: SharedGuard<T>) {
|
||||
// let new_data_ptr: DataPtrStruct<T>;
|
||||
// const old_data_ptr = AtomicValue.load(self.data_ptr);
|
||||
// do {
|
||||
// new_data_ptr = new DataPtrStruct(old_data_ptr.ptr, old_data_ptr.externalCounter + 1);
|
||||
// }
|
||||
// while (!AtomicValue.compareAndSwap(self.data_ptr, old_data_ptr, new_data_ptr));
|
||||
// }
|
||||
// }
|
||||
@@ -10,9 +10,7 @@ export class SharedLock<T extends SharedLockable | SharedMutex> {
|
||||
private _lockable: SharedLockable | undefined;
|
||||
private _ownsLock = false;
|
||||
|
||||
constructor();
|
||||
constructor(mutex: T, t?: "defer-lock" | "try-to-lock" | "adopt-lock");
|
||||
constructor(mutex?: T, t?: "defer-lock" | "try-to-lock" | "adopt-lock") {
|
||||
constructor(mutex?: T, t?: "lock" | "defer-lock" | "try-to-lock" | "adopt-lock") {
|
||||
this._mutex = mutex;
|
||||
this._lockable =
|
||||
mutex instanceof SharedMutex ? SharedMutex.asSharedLockable(mutex) :
|
||||
@@ -27,6 +25,7 @@ export class SharedLock<T extends SharedLockable | SharedMutex> {
|
||||
case "adopt-lock":
|
||||
this._ownsLock = true;
|
||||
break;
|
||||
case "lock":
|
||||
case undefined:
|
||||
this._lockable.lockShared();
|
||||
this._ownsLock = true;
|
||||
@@ -72,10 +71,10 @@ export class SharedLock<T extends SharedLockable | SharedMutex> {
|
||||
|
||||
swap(other: SharedLock<T>) {
|
||||
const mutex = other._mutex;
|
||||
other._mutex = this._mutex;
|
||||
const lockable = other._lockable;
|
||||
other._lockable = this._lockable;
|
||||
const ownsLock = other._ownsLock;
|
||||
other._mutex = this._mutex;
|
||||
other._lockable = this._lockable;
|
||||
other._ownsLock = this._ownsLock;
|
||||
this._mutex = mutex;
|
||||
this._lockable = lockable;
|
||||
@@ -84,12 +83,7 @@ export class SharedLock<T extends SharedLockable | SharedMutex> {
|
||||
|
||||
move() {
|
||||
const other = new SharedLock<T>();
|
||||
other._mutex = this._mutex;
|
||||
other._lockable = this._lockable;
|
||||
other._ownsLock = this._ownsLock;
|
||||
this._mutex = undefined;
|
||||
this._lockable = undefined;
|
||||
this._ownsLock = false;
|
||||
this.swap(other);
|
||||
return other;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "../symbolDisposeShim";
|
||||
|
||||
import { hasProperty } from "../core";
|
||||
import { Debug } from "../debug";
|
||||
import { Identifiable } from "../sharing/structs/identifiableStruct";
|
||||
import { Shared, SharedStructBase } from "../sharing/structs/sharedStruct";
|
||||
import { Tag, Tagged, TaggedStruct } from "../sharing/structs/taggedStruct";
|
||||
import { isTaggedStruct, Tag, Tagged } from "../sharing/structs/taggedStruct";
|
||||
import { Lockable } from "./lockable";
|
||||
import { SharedLockable } from "./sharedLockable";
|
||||
|
||||
@@ -216,9 +215,7 @@ export class SharedMutex extends Identifiable(Tagged(SharedStructBase, Tag.Share
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](value: unknown): value is SharedMutex {
|
||||
return typeof value === "object" && value !== null && // eslint-disable-line no-null/no-null -- necessary for comparision
|
||||
hasProperty(value, "__tag__") &&
|
||||
(value as TaggedStruct<Tag>).__tag__ === Tag.SharedMutex;
|
||||
return isTaggedStruct(value, Tag.SharedMutex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
let mutex: Atomics.Mutex;
|
||||
let condition: Atomics.Condition;
|
||||
let timeout: number;
|
||||
|
||||
/** @internal */
|
||||
export function sleep(ms: number) {
|
||||
mutex ??= new Atomics.Mutex();
|
||||
condition ??= new Atomics.Condition();
|
||||
timeout = ms;
|
||||
Atomics.Mutex.lock(mutex, sleepWorker);
|
||||
}
|
||||
|
||||
function sleepWorker() {
|
||||
Atomics.Condition.wait(condition, mutex, timeout | 0);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { sys } from "../sys";
|
||||
import { sleep } from "./sleep";
|
||||
|
||||
const cpuCount = sys.cpuCount?.() ?? 1;
|
||||
|
||||
export class SpinWait {
|
||||
private _count = 0;
|
||||
|
||||
reset() {
|
||||
this._count = 0;
|
||||
}
|
||||
|
||||
spinOnce() {
|
||||
const count = this._count;
|
||||
if (cpuCount > 0 && count < 10) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
// busy loop, do nothing
|
||||
}
|
||||
}
|
||||
else if ((count - 10) % 20 === 19) {
|
||||
sleep(1);
|
||||
}
|
||||
// else if ((count - 10) % 5 === 4) {
|
||||
// sleep(1);
|
||||
// }
|
||||
else {
|
||||
sleep(0);
|
||||
}
|
||||
this._count = (count + 1) >>> 0;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { WorkerThreadsHost, Worker, workerThreads } from "../workerThreads";
|
||||
import { SharedLinkedList } from "../sharing/collections/sharedLinkedList";
|
||||
import { Shared, SharedStructBase } from "../sharing/structs/sharedStruct";
|
||||
import { Mutex } from "./mutex";
|
||||
import { Condition } from "./condition";
|
||||
import { UniqueLock } from "./uniqueLock";
|
||||
import { combinePaths, getAnyExtensionFromPath, getBaseFileName, getDirectoryPath, isTaggedStruct, removeExtension, sys, Tag, Tagged } from "../_namespaces/ts";
|
||||
import { isNodeLikeSystem, noop } from "../core";
|
||||
import { Debug } from "../debug";
|
||||
import { isNodeLikeSystem } from "../core";
|
||||
import { CountdownEvent } from "./countdownEvent";
|
||||
import { Deque } from "../sharing/collections/deque";
|
||||
import { Shared, SharedStructBase } from "../sharing/structs/sharedStruct";
|
||||
import { Worker, workerThreads, WorkerThreadsHost } from "../workerThreads";
|
||||
import { Condition } from "./condition";
|
||||
import { Mutex } from "./mutex";
|
||||
import { SpinWait } from "./spinWait";
|
||||
import { UniqueLock } from "./uniqueLock";
|
||||
|
||||
const WORK_STEALING = true;
|
||||
const PER_THREAD_QUEUE = true;
|
||||
|
||||
@Shared()
|
||||
class ThreadPoolWorkItem extends SharedStructBase {
|
||||
class Task extends SharedStructBase {
|
||||
@Shared() readonly name: string;
|
||||
@Shared() readonly arg: Shareable;
|
||||
|
||||
@@ -20,40 +24,145 @@ class ThreadPoolWorkItem extends SharedStructBase {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Shared()
|
||||
export class ThreadPoolState extends SharedStructBase {
|
||||
@Shared() mutex = new Mutex();
|
||||
@Shared() condition = new Condition();
|
||||
@Shared() countdown = new CountdownEvent(1);
|
||||
@Shared() queue = new SharedLinkedList<ThreadPoolWorkItem>();
|
||||
@Shared() active = 0;
|
||||
class TaskQueue extends SharedStructBase {
|
||||
@Shared() private mutex = new Mutex();
|
||||
@Shared() private condition = new Condition();
|
||||
@Shared() private dequeue = new Deque<Task>();
|
||||
@Shared() done = false;
|
||||
@Shared() error: string | undefined;
|
||||
|
||||
static tryDequeue(self: TaskQueue) {
|
||||
return Deque.popBottom(self.dequeue);
|
||||
}
|
||||
|
||||
static dequeue(self: TaskQueue) {
|
||||
const done = self.done;
|
||||
const task = TaskQueue.tryDequeue(self);
|
||||
if (task || done) {
|
||||
return task;
|
||||
}
|
||||
|
||||
using lck = new UniqueLock(self.mutex);
|
||||
while (true) {
|
||||
const done = self.done;
|
||||
const task = TaskQueue.tryDequeue(self);
|
||||
if (task || done) {
|
||||
return task;
|
||||
}
|
||||
Condition.wait(self.condition, lck);
|
||||
}
|
||||
}
|
||||
|
||||
static steal(self: TaskQueue) {
|
||||
return Deque.steal(self.dequeue);
|
||||
}
|
||||
|
||||
static enqueue(self: TaskQueue, task: Task) {
|
||||
Debug.assert(!self.done);
|
||||
Deque.pushBottom(self.dequeue, task);
|
||||
Condition.notify(self.condition, 1);
|
||||
}
|
||||
|
||||
static done(self: TaskQueue) {
|
||||
self.done = true;
|
||||
Condition.notify(self.condition);
|
||||
}
|
||||
}
|
||||
|
||||
@Shared()
|
||||
class TaskScheduler extends SharedStructBase {
|
||||
@Shared() queues: SharedArray<TaskQueue>;
|
||||
@Shared() nextTaskId = 0;
|
||||
@Shared() mutex = new Mutex();
|
||||
|
||||
constructor(poolSize: number) {
|
||||
super();
|
||||
this.queues = new SharedArray(PER_THREAD_QUEUE ? poolSize : 1);
|
||||
const queueCount = this.queues.length;
|
||||
for (let i = 0; i < queueCount; i++) {
|
||||
this.queues[i] = new TaskQueue();
|
||||
}
|
||||
}
|
||||
|
||||
static shutdown(self: TaskScheduler) {
|
||||
const queueCount = self.queues.length;
|
||||
for (let i = 0; i < queueCount; i++) {
|
||||
TaskQueue.done(self.queues[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static scheduleTask(self: TaskScheduler, task: Task) {
|
||||
const queueCount = self.queues.length;
|
||||
const spin = new SpinWait();
|
||||
|
||||
let queueId: number;
|
||||
while (true) {
|
||||
const previousValue = Atomics.load(self, "nextTaskId");
|
||||
const nextValue = (previousValue + 1) >>> 0;
|
||||
if (Atomics.compareExchange(self, "nextTaskId", previousValue, nextValue) === previousValue) {
|
||||
queueId = previousValue % queueCount;
|
||||
break;
|
||||
}
|
||||
spin.spinOnce();
|
||||
}
|
||||
|
||||
TaskQueue.enqueue(self.queues[queueId], task);
|
||||
// TODO: wake all queues to steal work?
|
||||
}
|
||||
|
||||
static takeTask(self: TaskScheduler, queueId: number) {
|
||||
const queues = self.queues;
|
||||
|
||||
// first, try to take work from our queue
|
||||
const done = queues[queueId].done;
|
||||
const task = TaskQueue.tryDequeue(queues[queueId]);
|
||||
if (task || done) {
|
||||
return task;
|
||||
}
|
||||
|
||||
if (WORK_STEALING && PER_THREAD_QUEUE) {
|
||||
// next, try to steal work from an open queue
|
||||
const queueCount = queues.length;
|
||||
for (let i = 1; i < queueCount; i++) {
|
||||
const queue = queues[(queueId + i) % queueCount];
|
||||
const task = TaskQueue.steal(queue);
|
||||
if (task) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// finally, if that fails, perform a blocking dequeue on our queue
|
||||
return TaskQueue.dequeue(queues[queueId]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a thread pool of one or more worker threads. A {@link ThreadPool} can only be created on the main thread.
|
||||
* @internal
|
||||
*/
|
||||
export class ThreadPool {
|
||||
export class ThreadPool implements Disposable {
|
||||
readonly poolSize: number;
|
||||
|
||||
private readonly _host: WorkerThreadsHost;
|
||||
private readonly _generateCpuProfile: string | undefined;
|
||||
private _workers: Worker[] = [];
|
||||
private _state = new ThreadPoolState();
|
||||
private _threads: Thread[] = [];
|
||||
private _scheduler: TaskScheduler | undefined;
|
||||
|
||||
private _listening = false;
|
||||
private _onUncaughtException = () => {
|
||||
if (!this._state.done) {
|
||||
if (this._scheduler) {
|
||||
this.abort();
|
||||
}
|
||||
};
|
||||
|
||||
constructor(poolSize: number, host = workerThreads ?? Debug.fail("Worker threads not available.")) {
|
||||
constructor(poolSize: number, host = workerThreads ?? Debug.fail("Worker threads not available."), generateCpuProfile?: string) {
|
||||
Debug.assert(poolSize >= 1);
|
||||
Debug.assert(host.isMainThread(), "A new thread pool can only be created on the main thread.");
|
||||
this.poolSize = poolSize;
|
||||
this._generateCpuProfile = generateCpuProfile;
|
||||
this._scheduler = new TaskScheduler(poolSize);
|
||||
this._host = host;
|
||||
}
|
||||
|
||||
@@ -61,31 +170,71 @@ export class ThreadPool {
|
||||
* Starts all threads in the thread pool.
|
||||
*/
|
||||
start(): void {
|
||||
Debug.assert(this._scheduler, "Object is disposed");
|
||||
|
||||
if (this._workers.length < this.poolSize) {
|
||||
this._startListening();
|
||||
}
|
||||
|
||||
const queueCount = this._scheduler.queues.length;
|
||||
for (let i = this._workers.length; i < this.poolSize; i++) {
|
||||
this._workers.push(this._host.createWorker({
|
||||
name: "ThreadPool Thread",
|
||||
workerData: { type: "ThreadPoolThread", state: this._state }
|
||||
}));
|
||||
const thread = new Thread(this._scheduler, i % queueCount, this._generateCpuProfile);
|
||||
this._threads[i] = thread;
|
||||
|
||||
const worker = this._host.createWorker({ name: "ThreadPool Thread", workerData: thread });
|
||||
this._workers[i] = worker;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the addition of new work items in the thread pool and wait for threads to terminate gracefully.
|
||||
*/
|
||||
stop(timeout?: number) {
|
||||
this._shutdown();
|
||||
return CountdownEvent.wait(this._state.countdown, timeout);
|
||||
shutdown() {
|
||||
if (!this._scheduler) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._stopListening();
|
||||
|
||||
const scheduler = this._scheduler;
|
||||
const threads = this._threads.slice();
|
||||
|
||||
this._scheduler = undefined;
|
||||
this._threads.length = 0;
|
||||
this._workers.length = 0;
|
||||
|
||||
// mark all queues as done
|
||||
TaskScheduler.shutdown(scheduler);
|
||||
|
||||
// join all threads
|
||||
for (const thread of threads) {
|
||||
Thread.join(thread);
|
||||
}
|
||||
|
||||
Debug.log.trace("Thread pool shut down.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately stop all threads in the thread pool and wait for them to terminate.
|
||||
*/
|
||||
async abort() {
|
||||
this._shutdown();
|
||||
const workers = this._workers.splice(0, this._workers.length);
|
||||
if (!this._scheduler) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._stopListening();
|
||||
|
||||
const scheduler = this._scheduler;
|
||||
const workers = this._workers.slice();
|
||||
|
||||
this._scheduler = undefined;
|
||||
this._threads.length = 0;
|
||||
this._workers.length = 0;
|
||||
|
||||
// mark all queues as done
|
||||
TaskScheduler.shutdown(scheduler);
|
||||
|
||||
// terminate all workers
|
||||
await Promise.all(workers.map(worker => worker.terminate()));
|
||||
}
|
||||
|
||||
@@ -96,24 +245,8 @@ export class ThreadPool {
|
||||
* thread.
|
||||
*/
|
||||
queueWorkItem(name: string, arg?: Shareable) {
|
||||
{
|
||||
using _ = new UniqueLock(this._state.mutex);
|
||||
SharedLinkedList.push(this._state.queue, new ThreadPoolWorkItem(name, arg));
|
||||
}
|
||||
|
||||
Condition.notify(this._state.condition, 1);
|
||||
}
|
||||
|
||||
private _shutdown() {
|
||||
this._stopListening();
|
||||
|
||||
Debug.log.trace("Shutting down thread pool");
|
||||
if (!this._state.done) {
|
||||
using _ = new UniqueLock(this._state.mutex);
|
||||
this._state.done = true;
|
||||
Condition.notify(this._state.condition);
|
||||
CountdownEvent.signal(this._state.countdown, 1);
|
||||
}
|
||||
Debug.assert(this._scheduler, "Object is disposed");
|
||||
TaskScheduler.scheduleTask(this._scheduler, new Task(name, arg));
|
||||
}
|
||||
|
||||
private _startListening() {
|
||||
@@ -134,98 +267,102 @@ export class ThreadPool {
|
||||
process.off("uncaughtExceptionMonitor", this._onUncaughtException);
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a thread in a {@link ThreadPool} and can be used to process work items. A {@link ThreadPoolThread} should
|
||||
* only be used in a worker thread.
|
||||
* @internal
|
||||
* Entrypoint method for a thread pool thread, invoking the `processTask` callback whenever a task is available. This
|
||||
* function only exits once its associated queue is done adding new tasks and is empty. This function is only available
|
||||
* from a worker thread.
|
||||
*/
|
||||
export class ThreadPoolThread {
|
||||
private _state: ThreadPoolState;
|
||||
private _processWorkItem: (name: string, arg: Shareable) => void;
|
||||
export let runThread: (processTask: (name: string, arg: Shareable) => void) => void;
|
||||
|
||||
constructor(state: ThreadPoolState, processWorkItem: (name: string, arg: Shareable) => void) {
|
||||
this._state = state;
|
||||
this._processWorkItem = processWorkItem;
|
||||
/**
|
||||
* Queue a new task in a background thread in the thread pool. This function is only available from a worker thread.
|
||||
*/
|
||||
export let queueWorkItem: (name: string, arg?: Shareable) => void;
|
||||
|
||||
const enum ThreadState {
|
||||
NotStarted,
|
||||
Running,
|
||||
Exited,
|
||||
}
|
||||
|
||||
@Shared()
|
||||
class Thread extends Tagged(SharedStructBase, Tag.Thread) {
|
||||
@Shared() private mutex = new Mutex();
|
||||
@Shared() private condition = new Condition();
|
||||
@Shared() private scheduler: TaskScheduler;
|
||||
@Shared() private queueId: number;
|
||||
@Shared() private generateCpuProfile: string | undefined;
|
||||
@Shared() state = ThreadState.NotStarted;
|
||||
|
||||
constructor(scheduler: TaskScheduler, queueId: number, generateCpuProfile?: string) {
|
||||
super();
|
||||
this.scheduler = scheduler;
|
||||
this.queueId = queueId;
|
||||
this.generateCpuProfile = generateCpuProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the thread pool thread until the {@link ThreadPool} signals the thread should shut down.
|
||||
*/
|
||||
run() {
|
||||
let running = true;
|
||||
let started = false;
|
||||
try {
|
||||
const processWorkItem = this._processWorkItem;
|
||||
if (!CountdownEvent.tryAdd(this._state.countdown, 1)) {
|
||||
Debug.log.trace("thread pool is already shut down");
|
||||
return;
|
||||
static join(self: Thread) {
|
||||
using lck = new UniqueLock(self.mutex);
|
||||
Condition.wait(self.condition, lck, () => self.state === ThreadState.Exited);
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](value: unknown) {
|
||||
return isTaggedStruct(value, Tag.Thread);
|
||||
}
|
||||
|
||||
static {
|
||||
function runLoop(thread: Thread, processTask: (name: string, arg: Shareable) => void) {
|
||||
let task: Task | undefined;
|
||||
while (task = TaskScheduler.takeTask(thread.scheduler, thread.queueId)) {
|
||||
processTask(task.name, task.arg);
|
||||
}
|
||||
}
|
||||
|
||||
while (running) {
|
||||
let workItem: ThreadPoolWorkItem | undefined;
|
||||
{
|
||||
using lck = new UniqueLock(this._state.mutex);
|
||||
runThread = function (processTask) {
|
||||
Debug.assert(workerThreads?.isWorkerThread() && workerThreads.workerData instanceof Thread, "This function may only be called from a thread pool thread.");
|
||||
const thread = workerThreads.workerData;
|
||||
|
||||
// decrement the active thread counter before we start waiting for work
|
||||
if (started) {
|
||||
this._state.active--;
|
||||
}
|
||||
|
||||
// wait until we have work to do
|
||||
Condition.wait(this._state.condition, lck, () => this._state.queue.size > 0 || this._state.done);
|
||||
|
||||
// stop the thread if the thread pool is closed
|
||||
if (this._state.done) {
|
||||
if (started) {
|
||||
this._state.active--;
|
||||
}
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// increment the active thread counter before we start processing a work item
|
||||
this._state.active++;
|
||||
started = true;
|
||||
workItem = SharedLinkedList.shift(this._state.queue);
|
||||
}
|
||||
|
||||
// process the workitem
|
||||
if (workItem) {
|
||||
const state = thread.state;
|
||||
Debug.assert(state === ThreadState.NotStarted);
|
||||
Debug.assert(state === Atomics.compareExchange(thread, "state", state, ThreadState.Running));
|
||||
try {
|
||||
if (thread.generateCpuProfile && sys.enableCPUProfiler && sys.disableCPUProfiler) {
|
||||
const dirname = getDirectoryPath(thread.generateCpuProfile);
|
||||
const basename = getBaseFileName(thread.generateCpuProfile);
|
||||
const extname = getAnyExtensionFromPath(basename);
|
||||
const basenameNoExtension = removeExtension(basename, extname);
|
||||
const cpuProfilePath = combinePaths(dirname, `${basenameNoExtension}-${workerThreads.threadId}${extname}`);
|
||||
sys.enableCPUProfiler(cpuProfilePath, noop);
|
||||
try {
|
||||
processWorkItem(workItem.name, workItem.arg);
|
||||
runLoop(thread, processTask);
|
||||
}
|
||||
catch (e) {
|
||||
Debug.log.trace(e);
|
||||
running = false;
|
||||
using _ = new UniqueLock(this._state.mutex);
|
||||
this._state.active--;
|
||||
break;
|
||||
finally {
|
||||
sys.disableCPUProfiler(noop);
|
||||
}
|
||||
}
|
||||
else {
|
||||
runLoop(thread, processTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Debug.log.trace(e);
|
||||
}
|
||||
catch (e) {
|
||||
Debug.log.trace(e);
|
||||
}
|
||||
finally {
|
||||
thread.state = ThreadState.Exited;
|
||||
Condition.notify(thread.condition);
|
||||
}
|
||||
};
|
||||
|
||||
// Debug.log.trace(`shutting down.`);
|
||||
CountdownEvent.signal(this._state.countdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue additional work to be performed in another thread.
|
||||
*/
|
||||
queueWorkItem(name: string, arg?: Shareable) {
|
||||
Debug.assert(!this._state.done);
|
||||
|
||||
{
|
||||
using _ = new UniqueLock(this._state.mutex);
|
||||
Debug.assert(!this._state.done);
|
||||
SharedLinkedList.push(this._state.queue, new ThreadPoolWorkItem(name, arg));
|
||||
}
|
||||
|
||||
Condition.notify(this._state.condition, 1);
|
||||
queueWorkItem = function (name, arg) {
|
||||
Debug.assert(workerThreads?.isWorkerThread() && workerThreads.workerData instanceof Thread, "This function may only be called from a thread pool thread.");
|
||||
const thread = workerThreads.workerData;
|
||||
TaskScheduler.scheduleTask(thread.scheduler, new Task(name, arg));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
import "../symbolDisposeShim";
|
||||
|
||||
import { hasProperty } from "../core";
|
||||
import { Debug } from "../debug";
|
||||
import { BasicLockable, Lockable } from "./lockable";
|
||||
import { Lockable } from "./lockable";
|
||||
import { Mutex } from "./mutex";
|
||||
import { SharedMutex } from "./sharedMutex";
|
||||
|
||||
function isLockable(x: BasicLockable): x is Lockable {
|
||||
return x instanceof UniqueLock ? !!x.mutex && isLockable(x.mutex) : hasProperty(x, "tryLock") && typeof (x as Lockable).tryLock === "function";
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class UniqueLock<T extends BasicLockable | Mutex | SharedMutex> {
|
||||
export class UniqueLock<T extends Mutex | SharedMutex> {
|
||||
private _mutex: T | undefined;
|
||||
private _lockable: BasicLockable | undefined;
|
||||
private _lockable: Lockable | undefined;
|
||||
private _ownsLock = false;
|
||||
|
||||
constructor(mutex?: T);
|
||||
constructor(mutex: Extract<T, Lockable | Mutex | SharedMutex>, t?: "defer-lock" | "try-to-lock" | "adopt-lock");
|
||||
constructor(mutex: T, t?: "defer-lock" | "adopt-lock");
|
||||
constructor(mutex?: T, t?: "defer-lock" | "try-to-lock" | "adopt-lock") {
|
||||
constructor(mutex?: T, t?: "lock" | "defer-lock" | "try-to-lock" | "adopt-lock") {
|
||||
this._mutex = mutex;
|
||||
this._lockable =
|
||||
mutex instanceof Mutex ? Mutex.asLockable(mutex) :
|
||||
@@ -30,12 +22,12 @@ export class UniqueLock<T extends BasicLockable | Mutex | SharedMutex> {
|
||||
case "defer-lock":
|
||||
break;
|
||||
case "try-to-lock":
|
||||
Debug.assert(isLockable(this._lockable));
|
||||
this._ownsLock = this._lockable.tryLock();
|
||||
break;
|
||||
case "adopt-lock":
|
||||
this._ownsLock = true;
|
||||
break;
|
||||
case "lock":
|
||||
case undefined:
|
||||
this._lockable.lock();
|
||||
this._ownsLock = true;
|
||||
@@ -55,7 +47,6 @@ export class UniqueLock<T extends BasicLockable | Mutex | SharedMutex> {
|
||||
tryLock(this: UniqueLock<Extract<T, Lockable | Mutex | SharedMutex>>): boolean {
|
||||
Debug.assert(this._lockable);
|
||||
Debug.assert(!this._ownsLock);
|
||||
Debug.assert(isLockable(this._lockable));
|
||||
this._ownsLock = this._lockable.tryLock();
|
||||
return this._ownsLock;
|
||||
}
|
||||
@@ -94,12 +85,7 @@ export class UniqueLock<T extends BasicLockable | Mutex | SharedMutex> {
|
||||
|
||||
move() {
|
||||
const other = new UniqueLock<T>();
|
||||
other._mutex = this._mutex;
|
||||
other._lockable = this._lockable;
|
||||
other._ownsLock = this._ownsLock;
|
||||
this._mutex = undefined;
|
||||
this._lockable = undefined;
|
||||
this._ownsLock = false;
|
||||
this.swap(other);
|
||||
return other;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,6 +170,18 @@ export namespace tracingEnabled {
|
||||
}
|
||||
eventStack.length = 0;
|
||||
}
|
||||
|
||||
const popActivity = {
|
||||
[Symbol.dispose]() {
|
||||
pop();
|
||||
}
|
||||
};
|
||||
|
||||
export function traceActivity(phase: Phase, name: string, args?: Args, separateBeginAndEnd?: boolean): Disposable {
|
||||
push(phase, name, args, separateBeginAndEnd);
|
||||
return popActivity;
|
||||
}
|
||||
|
||||
// sample every 10ms
|
||||
const sampleInterval = 1000 * 10;
|
||||
function writeStackEvent(index: number, endTime: number, results?: Args) {
|
||||
|
||||
@@ -347,8 +347,10 @@ export function createSolutionBuilder<T extends BuilderProgram>(host: SolutionBu
|
||||
return createSolutionBuilderWorker(/*watch*/ false, host, rootNames, defaultOptions);
|
||||
}
|
||||
|
||||
export function createSolutionBuilderWithWatch<T extends BuilderProgram>(host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T> {
|
||||
return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions, baseWatchOptions);
|
||||
export function createSolutionBuilderWithWatch<T extends BuilderProgram>(host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T>;
|
||||
/** @internal */ export function createSolutionBuilderWithWatch<T extends BuilderProgram>(host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions, threadPoolLifetime?: DisposableStack): SolutionBuilder<T>;
|
||||
export function createSolutionBuilderWithWatch<T extends BuilderProgram>(host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions, threadPoolLifetime?: DisposableStack): SolutionBuilder<T> {
|
||||
return createSolutionBuilderWorker(/*watch*/ true, host, rootNames, defaultOptions, baseWatchOptions, threadPoolLifetime);
|
||||
}
|
||||
|
||||
type ConfigFileCacheEntry = ParsedCommandLine | Diagnostic;
|
||||
@@ -430,9 +432,11 @@ interface SolutionBuilderState<T extends BuilderProgram> extends WatchFactory<Wa
|
||||
timerToBuildInvalidatedProject: any;
|
||||
reportFileChangeDetected: boolean;
|
||||
writeLog: (s: string) => void;
|
||||
|
||||
readonly threadPoolLifetime: DisposableStack | undefined;
|
||||
}
|
||||
|
||||
function createSolutionBuilderState<T extends BuilderProgram>(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost<T> | SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], options: BuildOptions, baseWatchOptions: WatchOptions | undefined): SolutionBuilderState<T> {
|
||||
function createSolutionBuilderState<T extends BuilderProgram>(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost<T> | SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], options: BuildOptions, baseWatchOptions: WatchOptions | undefined, threadPoolLifetime: DisposableStack | undefined): SolutionBuilderState<T> {
|
||||
const host = hostOrHostWithWatch as SolutionBuilderHost<T>;
|
||||
const hostWithWatch = hostOrHostWithWatch as SolutionBuilderWithWatchHost<T>;
|
||||
|
||||
@@ -546,6 +550,9 @@ function createSolutionBuilderState<T extends BuilderProgram>(watch: boolean, ho
|
||||
watchFile,
|
||||
watchDirectory,
|
||||
writeLog,
|
||||
|
||||
// once we've reached this point we can take ownership of the threadPool's lifetime
|
||||
threadPoolLifetime: threadPoolLifetime?.move(),
|
||||
};
|
||||
|
||||
return state;
|
||||
@@ -2455,6 +2462,7 @@ function startWatching<T extends BuilderProgram>(state: SolutionBuilderState<T>,
|
||||
}
|
||||
|
||||
function stopWatching<T extends BuilderProgram>(state: SolutionBuilderState<T>) {
|
||||
using _ = state.threadPoolLifetime;
|
||||
clearMap(state.allWatchedConfigFiles, closeFileWatcher);
|
||||
clearMap(state.allWatchedExtendedConfigFiles, closeFileWatcherOf);
|
||||
clearMap(state.allWatchedWildcardDirectories, watchedWildcardDirectories => clearMap(watchedWildcardDirectories, closeFileWatcherOf));
|
||||
@@ -2467,9 +2475,9 @@ function stopWatching<T extends BuilderProgram>(state: SolutionBuilderState<T>)
|
||||
* can dynamically add/remove other projects based on changes on the rootNames' references
|
||||
*/
|
||||
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: false, host: SolutionBuilderHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions): SolutionBuilder<T>;
|
||||
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: true, host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T>;
|
||||
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost<T> | SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions): SolutionBuilder<T> {
|
||||
const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions);
|
||||
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: true, host: SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], defaultOptions: BuildOptions, baseWatchOptions?: WatchOptions, threadPoolLifetime?: DisposableStack): SolutionBuilder<T>;
|
||||
function createSolutionBuilderWorker<T extends BuilderProgram>(watch: boolean, hostOrHostWithWatch: SolutionBuilderHost<T> | SolutionBuilderWithWatchHost<T>, rootNames: readonly string[], options: BuildOptions, baseWatchOptions?: WatchOptions, threadPoolLifetime?: DisposableStack): SolutionBuilder<T> {
|
||||
const state = createSolutionBuilderState(watch, hostOrHostWithWatch, rootNames, options, baseWatchOptions, threadPoolLifetime);
|
||||
return {
|
||||
build: (project, cancellationToken, writeFile, getCustomTransformers) => build(state, project, cancellationToken, writeFile, getCustomTransformers),
|
||||
clean: project => clean(state, project),
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"extends": "../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"experimentalDecorators": true
|
||||
"experimentalDecorators": true,
|
||||
"strictBindCallApply": true
|
||||
},
|
||||
|
||||
"references": [
|
||||
|
||||
@@ -20,8 +20,7 @@ import {
|
||||
ThreadPool,
|
||||
WorkerThreadsHost,
|
||||
} from "./_namespaces/ts";
|
||||
import { SharedNodeBase } from "./sharing/sharedNode";
|
||||
import { SharedParserState } from "./sharing/sharedParserState";
|
||||
import { SharedNode } from "./sharing/sharedNode";
|
||||
|
||||
// branded string type used to store absolute, normalized and canonicalized paths
|
||||
// arbitrary file name can be converted to Path via toPath function
|
||||
@@ -931,7 +930,7 @@ export interface Node extends ReadonlyTextRange {
|
||||
// `locals` and `nextContainer` have been moved to `LocalsContainer`
|
||||
// `flowNode` has been moved to `FlowContainer`
|
||||
// see: https://github.com/microsoft/TypeScript/pull/51682
|
||||
/** @internal */ __shared__?: SharedNodeBase;
|
||||
/** @internal */ __shared__?: SharedNode;
|
||||
}
|
||||
|
||||
export interface JSDocContainer extends Node {
|
||||
@@ -7772,7 +7771,7 @@ export type HasInvalidatedLibResolutions = (libFileName: string) => boolean;
|
||||
export type HasChangedAutomaticTypeDirectiveNames = () => boolean;
|
||||
|
||||
export interface CompilerHost extends ModuleResolutionHost {
|
||||
/** @internal */ requestSourceFile?(parserState: SharedParserState, fileName: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, shouldCreateNewSourceFile?: boolean, setFileVersion?: boolean): void;
|
||||
/** @internal */ requestSourceFile?(fileName: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, shouldCreateNewSourceFile?: boolean, setFileVersion?: boolean): void;
|
||||
getSourceFile(fileName: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined;
|
||||
getSourceFileByPath?(fileName: string, path: Path, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined;
|
||||
getCancellationToken?(): CancellationToken;
|
||||
|
||||
+7
-12
@@ -18,7 +18,6 @@ import {
|
||||
createIncrementalCompilerHost,
|
||||
createIncrementalProgram,
|
||||
CreateProgram,
|
||||
createRequestSourceFile,
|
||||
createWriteFileMeasuringIO,
|
||||
CustomTransformers,
|
||||
Debug,
|
||||
@@ -73,6 +72,7 @@ import {
|
||||
isReferenceFileLocation,
|
||||
isString,
|
||||
last,
|
||||
makeCompilerHostParallel,
|
||||
maybeBind,
|
||||
memoize,
|
||||
ModuleKind,
|
||||
@@ -108,7 +108,7 @@ import {
|
||||
WatchStatusReporter,
|
||||
whitespaceOrMapCommentRegExp,
|
||||
WorkerThreadsHost,
|
||||
WriteFileCallback,
|
||||
WriteFileCallback
|
||||
} from "./_namespaces/ts";
|
||||
|
||||
const sysFormatDiagnosticsHost: FormatDiagnosticsHost | undefined = sys ? {
|
||||
@@ -744,7 +744,6 @@ export function createWatchFactory<Y = undefined>(host: WatchFactoryHost & { tra
|
||||
export function createCompilerHostFromProgramHost(host: ProgramHost<any>, getCompilerOptions: () => CompilerOptions, directoryStructureHost: DirectoryStructureHost = host): CompilerHost {
|
||||
const useCaseSensitiveFileNames = host.useCaseSensitiveFileNames();
|
||||
const compilerHost: CompilerHost = {
|
||||
requestSourceFile: host.threadPool && createRequestSourceFile(host.threadPool, /*setParentNodes*/ undefined),
|
||||
getSourceFile: createGetSourceFile(
|
||||
(fileName, encoding) => !encoding ? compilerHost.readFile(fileName) : host.readFile(fileName, encoding),
|
||||
getCompilerOptions,
|
||||
@@ -773,8 +772,10 @@ export function createCompilerHostFromProgramHost(host: ProgramHost<any>, getCom
|
||||
readDirectory: maybeBind(host, host.readDirectory),
|
||||
storeFilesChangingSignatureDuringEmit: host.storeFilesChangingSignatureDuringEmit,
|
||||
workerThreads: host.workerThreads,
|
||||
threadPool: host.threadPool,
|
||||
};
|
||||
if (host.threadPool) {
|
||||
makeCompilerHostParallel(compilerHost, host.threadPool, /*setParentNodes*/ undefined);
|
||||
}
|
||||
return compilerHost;
|
||||
}
|
||||
|
||||
@@ -821,18 +822,12 @@ export function getSourceFileVersionAsHashFromText(host: Pick<CompilerHost, "cre
|
||||
export function setGetSourceFileAsHashVersioned(compilerHost: CompilerHost) {
|
||||
const originalGetSourceFile = compilerHost.getSourceFile;
|
||||
compilerHost.getSourceFile = (...args) => {
|
||||
const result = originalGetSourceFile.call(compilerHost, ...args);
|
||||
const result = originalGetSourceFile.call(compilerHost, ...args) as SourceFile | undefined;
|
||||
if (result) {
|
||||
result.version = getSourceFileVersionAsHashFromText(compilerHost, result.text);
|
||||
result.version ??= getSourceFileVersionAsHashFromText(compilerHost, result.text);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const originalRequestSourceFile = compilerHost.requestSourceFile;
|
||||
if (originalRequestSourceFile) {
|
||||
compilerHost.requestSourceFile = (parserState, fileName, languageVersionOrOptions, shouldCreateNewSourceFile) => {
|
||||
originalRequestSourceFile.call(compilerHost, parserState, fileName, languageVersionOrOptions, shouldCreateNewSourceFile, /*setFileVersion*/ true);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -410,9 +410,11 @@ type WatchCompilerHostOfFilesAndCompilerOptionsOrConfigFile<T extends BuilderPro
|
||||
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfFilesAndCompilerOptions<T>): WatchOfFilesAndCompilerOptions<T>;
|
||||
/**
|
||||
* Creates the watch from the host for config file
|
||||
*/
|
||||
*/
|
||||
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfConfigFile<T>): WatchOfConfigFile<T>;
|
||||
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfFilesAndCompilerOptionsOrConfigFile<T>): WatchOfFilesAndCompilerOptions<T> | WatchOfConfigFile<T> {
|
||||
/** @internal */ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfFilesAndCompilerOptions<T>, threadPoolLifetime?: DisposableStack): WatchOfFilesAndCompilerOptions<T>;
|
||||
/** @internal */ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfConfigFile<T>, threadPoolLifetime?: DisposableStack): WatchOfConfigFile<T>;
|
||||
export function createWatchProgram<T extends BuilderProgram>(host: WatchCompilerHostOfFilesAndCompilerOptionsOrConfigFile<T>, _threadPoolLifetime?: DisposableStack): WatchOfFilesAndCompilerOptions<T> | WatchOfConfigFile<T> {
|
||||
interface FilePresentOnHost {
|
||||
version: string;
|
||||
sourceFile: SourceFile;
|
||||
@@ -549,11 +551,15 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
|
||||
// Update extended config file watch
|
||||
if (configFileName) updateExtendedConfigFilesWatches(toPath(configFileName), compilerOptions, watchOptions, WatchType.ExtendedConfigFile);
|
||||
|
||||
// once we've reached this point we can take ownership of the thread pool's lifetime
|
||||
const threadPoolLifetime = _threadPoolLifetime?.move();
|
||||
|
||||
return configFileName ?
|
||||
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, close } :
|
||||
{ getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close };
|
||||
|
||||
function close() {
|
||||
using _ = threadPoolLifetime;
|
||||
clearInvalidateResolutionsOfFailedLookupLocations();
|
||||
resolutionCache.clear();
|
||||
clearMap(sourceFilesCache, value => {
|
||||
|
||||
@@ -689,20 +689,17 @@ export function getWatchFactory<X, Y = undefined>(host: WatchFactoryHost, watchL
|
||||
|
||||
return {
|
||||
watchFile: createExcludeHandlingAddWatch("watchFile"),
|
||||
watchDirectory: createExcludeHandlingAddWatch("watchDirectory")
|
||||
watchDirectory: createExcludeHandlingAddWatch("watchDirectory"),
|
||||
};
|
||||
|
||||
function createExcludeHandlingAddWatch<T extends keyof WatchFactory<X, Y>>(key: T): WatchFactory<X, Y>[T] {
|
||||
return (
|
||||
file: string,
|
||||
cb: FileWatcherCallback | DirectoryWatcherCallback,
|
||||
flags: PollingInterval | WatchDirectoryFlags,
|
||||
options: WatchOptions | undefined,
|
||||
detailInfo1: X,
|
||||
detailInfo2?: Y
|
||||
) => !matchesExclude(file, key === "watchFile" ? options?.excludeFiles : options?.excludeDirectories, useCaseSensitiveFileNames(), host.getCurrentDirectory?.() || "") ?
|
||||
factory[key].call(/*thisArgs*/ undefined, file, cb, flags, options, detailInfo1, detailInfo2) :
|
||||
function createExcludeHandlingAddWatch<K extends keyof WatchFactory<X, Y>>(key: K): WatchCallback<K> {
|
||||
return (...args) => {
|
||||
const [file, , flags, options, detailInfo1, detailInfo2] = args;
|
||||
const f = factory[key] as WatchCallback<K>;
|
||||
return !matchesExclude(file, key === "watchFile" ? options?.excludeFiles : options?.excludeDirectories, useCaseSensitiveFileNames(), host.getCurrentDirectory?.() || "") ?
|
||||
f(...args) :
|
||||
excludeWatcherFactory(file, flags, options, detailInfo1, detailInfo2);
|
||||
};
|
||||
}
|
||||
|
||||
function useCaseSensitiveFileNames() {
|
||||
@@ -768,22 +765,26 @@ export function getWatchFactory<X, Y = undefined>(host: WatchFactoryHost, watchL
|
||||
};
|
||||
}
|
||||
|
||||
function createTriggerLoggingAddWatch<T extends keyof WatchFactory<X, Y>>(key: T): WatchFactory<X, Y>[T] {
|
||||
return (
|
||||
file: string,
|
||||
cb: FileWatcherCallback | DirectoryWatcherCallback,
|
||||
flags: PollingInterval | WatchDirectoryFlags,
|
||||
options: WatchOptions | undefined,
|
||||
detailInfo1: X,
|
||||
detailInfo2?: Y
|
||||
) => plainInvokeFactory[key].call(/*thisArgs*/ undefined, file, (...args: any[]) => {
|
||||
type WatchCallback<K extends keyof WatchFactory<X, Y>> = (...args: Parameters<WatchFactory<X, Y>[K]>) => FileWatcher;
|
||||
|
||||
function addTriggerLoggingToCallback<A extends any[]>(cb: (...args: A) => void, key: keyof WatchFactory<X, Y>, file: string, flags: PollingInterval | WatchDirectoryFlags, options: WatchOptions | undefined, detailInfo1: X, detailInfo2: Y | undefined): (...args: A) => void {
|
||||
return (...args: A) => {
|
||||
const triggerredInfo = `${key === "watchFile" ? "FileWatcher" : "DirectoryWatcher"}:: Triggered with ${args[0]} ${args[1] !== undefined ? args[1] : ""}:: ${getWatchInfo(file, flags, options, detailInfo1, detailInfo2, getDetailWatchInfo)}`;
|
||||
log(triggerredInfo);
|
||||
const start = timestamp();
|
||||
cb.call(/*thisArg*/ undefined, ...args);
|
||||
cb(...args);
|
||||
const elapsed = timestamp() - start;
|
||||
log(`Elapsed:: ${elapsed}ms ${triggerredInfo}`);
|
||||
}, flags, options, detailInfo1, detailInfo2);
|
||||
};
|
||||
}
|
||||
|
||||
function createTriggerLoggingAddWatch<K extends keyof WatchFactory<X, Y>>(key: K): WatchCallback<K> {
|
||||
return (...args) => {
|
||||
const [file, cb, flags, options, detailInfo1, detailInfo2] = args;
|
||||
const f = plainInvokeFactory[key] as WatchCallback<K>;
|
||||
args[1] = addTriggerLoggingToCallback(cb, key, file, flags, options, detailInfo1, detailInfo2);
|
||||
return f(...args);
|
||||
};
|
||||
}
|
||||
|
||||
function getWatchInfo<T>(file: string, flags: T, options: WatchOptions | undefined, detailInfo1: X, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo<X, Y> | undefined) {
|
||||
|
||||
+12
-23
@@ -1,4 +1,4 @@
|
||||
import { createCompilerHostWorker, Debug, isNodeLikeSystem, setGetSourceFileAsHashVersioned, sys, System, ThreadPoolState, ThreadPoolThread, workerThreads, WorkerThreadWorkerThreadsHost } from "./_namespaces/ts";
|
||||
import { createCompilerHostWorker, Debug, isNodeLikeSystem, runThread, setGetSourceFileAsHashVersioned, setSys, setWorkerThreadsHost, System, workerThreads, WorkerThreadWorkerThreadsHost } from "./_namespaces/ts";
|
||||
import { SharedSourceFile } from "./sharing/sharedNode";
|
||||
import { getSharedObjectAllocator } from "./sharing/sharedObjectAllocator";
|
||||
import { SharedSourceFileEntry } from "./sharing/sharedParserState";
|
||||
@@ -6,19 +6,21 @@ import { Condition } from "./threading/condition";
|
||||
import { UniqueLock } from "./threading/uniqueLock";
|
||||
|
||||
/** @internal */
|
||||
export function executeWorker(_system: System, host: WorkerThreadWorkerThreadsHost) {
|
||||
export function executeWorker(system: System, host: WorkerThreadWorkerThreadsHost) {
|
||||
// if (process.execArgv.includes("--inspect-brk")) {
|
||||
// const inspector = require("node:inspector") as typeof import("node:inspector");
|
||||
// inspector.open();
|
||||
// inspector.waitForDebugger();
|
||||
// }
|
||||
|
||||
setSys(system);
|
||||
setWorkerThreadsHost(host);
|
||||
if (isNodeLikeSystem()) {
|
||||
const fs: typeof import("fs") = require("fs");
|
||||
const util: typeof import("util") = require("util");
|
||||
Debug.loggingHost = {
|
||||
log(_level, s) {
|
||||
fs.writeSync(2, `[worker#${host.threadId}] ${s || ""}${sys.newLine}`);
|
||||
fs.writeSync(2, `[worker#${host.threadId}] ${s || ""}${system.newLine}`);
|
||||
},
|
||||
format(...args) {
|
||||
return util.formatWithOptions({ colors: true }, ...args);
|
||||
@@ -26,26 +28,13 @@ export function executeWorker(_system: System, host: WorkerThreadWorkerThreadsHo
|
||||
};
|
||||
}
|
||||
|
||||
const { workerData } = host;
|
||||
Debug.assert(workerData);
|
||||
const { type } = workerData as { type: string };
|
||||
switch (type) {
|
||||
case "ThreadPoolThread": {
|
||||
const { state } = workerData as { state: ThreadPoolState };
|
||||
const thread = new ThreadPoolThread(state, (name, arg) => {
|
||||
switch (name) {
|
||||
case "Program.requestSourceFile":
|
||||
programRequestSourceFile(arg as SharedSourceFileEntry);
|
||||
break;
|
||||
}
|
||||
});
|
||||
thread.run();
|
||||
break;
|
||||
runThread((name, arg) => {
|
||||
switch (name) {
|
||||
case "Program.requestSourceFile":
|
||||
programRequestSourceFile(arg as SharedSourceFileEntry);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Debug.fail(`Unsupported worker type: '${type}'.`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function programRequestSourceFile(entry: SharedSourceFileEntry) {
|
||||
let ok = false;
|
||||
@@ -53,7 +42,7 @@ export function executeWorker(_system: System, host: WorkerThreadWorkerThreadsHo
|
||||
try {
|
||||
// Debug.log.trace(`parsing: ${entry.fileName}`);
|
||||
const overideObjectAllocator = getSharedObjectAllocator();
|
||||
const host = createCompilerHostWorker({}, entry.setParentNodes, sys, workerThreads, /*threadPool*/ undefined, overideObjectAllocator);
|
||||
const host = createCompilerHostWorker({}, entry.setParentNodes, system, workerThreads, /*threadPool*/ undefined, overideObjectAllocator);
|
||||
if (entry.setFileVersion) {
|
||||
setGetSourceFileAsHashVersioned(host);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as performance from "../compiler/performance";
|
||||
import { createDisposableStack } from "../compiler/symbolDisposeShim";
|
||||
import {
|
||||
arrayFrom,
|
||||
BuilderProgram,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
DiagnosticMessage,
|
||||
DiagnosticReporter,
|
||||
Diagnostics,
|
||||
dispose,
|
||||
dumpTracingLegend,
|
||||
EmitAndSemanticDiagnosticsBuilderProgram,
|
||||
emitFilesAndReportErrorsAndGetExitStatus,
|
||||
@@ -638,12 +640,8 @@ function executeCommandLineWorker(
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
|
||||
let threadPool: ThreadPool | undefined;
|
||||
const maxCpuCount = getMaxCpuCount(commandLine.options);
|
||||
if (maxCpuCount > 1 && workerThreads?.isMainThread()) {
|
||||
threadPool = new ThreadPool(maxCpuCount, workerThreads);
|
||||
threadPool.start();
|
||||
}
|
||||
using threadPoolLifetime = createDisposableStack();
|
||||
const threadPool = threadPoolLifetime.use(tryCreateThreadPool(commandLine.options, workerThreads));
|
||||
|
||||
const currentDirectory = sys.getCurrentDirectory();
|
||||
const commandLineOptions = convertToOptionsWithAbsolutePaths(
|
||||
@@ -661,10 +659,12 @@ function executeCommandLineWorker(
|
||||
configParseResult.options
|
||||
);
|
||||
configParseResult.errors.forEach(reportDiagnostic);
|
||||
dispose(threadPoolLifetime);
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
sys.write(JSON.stringify(convertToTSConfig(configParseResult, configFileName, sys), null, 4) + sys.newLine);
|
||||
dispose(threadPoolLifetime);
|
||||
return sys.exit(ExitStatus.Success);
|
||||
}
|
||||
reportDiagnostic = updateReportDiagnostic(
|
||||
@@ -684,6 +684,7 @@ function executeCommandLineWorker(
|
||||
extendedConfigCache,
|
||||
workerThreads,
|
||||
threadPool,
|
||||
threadPoolLifetime, // hand off the lifetime of the thread pool
|
||||
);
|
||||
}
|
||||
else if (isIncrementalCompilation(configParseResult.options)) {
|
||||
@@ -694,6 +695,7 @@ function executeCommandLineWorker(
|
||||
configParseResult,
|
||||
workerThreads,
|
||||
threadPool,
|
||||
threadPoolLifetime,
|
||||
);
|
||||
}
|
||||
else {
|
||||
@@ -704,6 +706,7 @@ function executeCommandLineWorker(
|
||||
configParseResult,
|
||||
workerThreads,
|
||||
threadPool,
|
||||
threadPoolLifetime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -711,6 +714,7 @@ function executeCommandLineWorker(
|
||||
if (commandLineOptions.showConfig) {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
sys.write(JSON.stringify(convertToTSConfig(commandLine, combinePaths(currentDirectory, "tsconfig.json"), sys), null, 4) + sys.newLine);
|
||||
dispose(threadPoolLifetime);
|
||||
return sys.exit(ExitStatus.Success);
|
||||
}
|
||||
reportDiagnostic = updateReportDiagnostic(
|
||||
@@ -729,6 +733,7 @@ function executeCommandLineWorker(
|
||||
commandLine.watchOptions,
|
||||
workerThreads,
|
||||
threadPool,
|
||||
threadPoolLifetime // hand off the lifetime of the thread pool
|
||||
);
|
||||
}
|
||||
else if (isIncrementalCompilation(commandLineOptions)) {
|
||||
@@ -739,6 +744,7 @@ function executeCommandLineWorker(
|
||||
{ ...commandLine, options: commandLineOptions },
|
||||
workerThreads,
|
||||
threadPool,
|
||||
threadPoolLifetime,
|
||||
);
|
||||
}
|
||||
else {
|
||||
@@ -749,6 +755,7 @@ function executeCommandLineWorker(
|
||||
{ ...commandLine, options: commandLineOptions },
|
||||
workerThreads,
|
||||
threadPool,
|
||||
threadPoolLifetime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -818,6 +825,15 @@ function reportWatchModeWithoutSysSupport(sys: System, reportDiagnostic: Diagnos
|
||||
return false;
|
||||
}
|
||||
|
||||
function tryCreateThreadPool(options: BuildOptions | CompilerOptions, workerThreads: WorkerThreadsHost | undefined) {
|
||||
const maxCpuCount = getMaxCpuCount(options);
|
||||
if (maxCpuCount > 1 && workerThreads?.isMainThread()) {
|
||||
const threadPool = new ThreadPool(maxCpuCount, workerThreads, options.generateCpuProfile);
|
||||
threadPool.start();
|
||||
return threadPool;
|
||||
}
|
||||
}
|
||||
|
||||
function performBuild(
|
||||
sys: System,
|
||||
cb: ExecuteCommandLineCallbacks,
|
||||
@@ -860,15 +876,10 @@ function performBuild(
|
||||
return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped);
|
||||
}
|
||||
|
||||
let threadPool: ThreadPool | undefined;
|
||||
const maxCpuCount = getMaxCpuCount(buildOptions);
|
||||
if (maxCpuCount > 1 && workerThreads?.isMainThread()) {
|
||||
threadPool = new ThreadPool(maxCpuCount, workerThreads);
|
||||
}
|
||||
|
||||
if (buildOptions.watch) {
|
||||
if (reportWatchModeWithoutSysSupport(sys, reportDiagnostic)) return;
|
||||
threadPool?.start();
|
||||
using threadPoolLifetime = createDisposableStack();
|
||||
const threadPool = threadPoolLifetime.use(tryCreateThreadPool(buildOptions, workerThreads));
|
||||
const buildHost = createSolutionBuilderWithWatchHost(
|
||||
sys,
|
||||
/*createProgram*/ undefined,
|
||||
@@ -891,14 +902,15 @@ function performBuild(
|
||||
reportSolutionBuilderTimes(builder, solutionPerformance);
|
||||
}
|
||||
};
|
||||
const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions, watchOptions);
|
||||
const builder = createSolutionBuilderWithWatch(buildHost, projects, buildOptions, watchOptions, threadPoolLifetime);
|
||||
builder.build();
|
||||
reportSolutionBuilderTimes(builder, solutionPerformance);
|
||||
reportBuildStatistics = true;
|
||||
return builder;
|
||||
}
|
||||
|
||||
threadPool?.start();
|
||||
using threadPoolLifetime = createDisposableStack();
|
||||
const threadPool = threadPoolLifetime.use(tryCreateThreadPool(buildOptions, workerThreads));
|
||||
const buildHost = createSolutionBuilderHost(
|
||||
sys,
|
||||
/*createProgram*/ undefined,
|
||||
@@ -914,7 +926,7 @@ function performBuild(
|
||||
const exitStatus = buildOptions.clean ? builder.clean() : builder.build();
|
||||
reportSolutionBuilderTimes(builder, solutionPerformance);
|
||||
dumpTracingLegend(); // Will no-op if there hasn't been any tracing
|
||||
threadPool?.stop();
|
||||
dispose(threadPoolLifetime);
|
||||
return sys.exit(exitStatus);
|
||||
}
|
||||
|
||||
@@ -931,6 +943,7 @@ function performCompilation(
|
||||
config: ParsedCommandLine,
|
||||
workerThreads: WorkerThreadsHost | undefined,
|
||||
threadPool: ThreadPool | undefined,
|
||||
threadPoolLifetime: DisposableStack | undefined,
|
||||
) {
|
||||
const { fileNames, options, projectReferences } = config;
|
||||
const host = createCompilerHostWorker(options, /*setParentNodes*/ undefined, sys, workerThreads, threadPool);
|
||||
@@ -955,7 +968,7 @@ function performCompilation(
|
||||
);
|
||||
reportStatistics(sys, program, /*solutionPerformance*/ undefined);
|
||||
cb(program);
|
||||
threadPool?.stop();
|
||||
dispose(threadPoolLifetime);
|
||||
return sys.exit(exitStatus);
|
||||
}
|
||||
|
||||
@@ -966,6 +979,7 @@ function performIncrementalCompilation(
|
||||
config: ParsedCommandLine,
|
||||
workerThreads: WorkerThreadsHost | undefined,
|
||||
threadPool: ThreadPool | undefined,
|
||||
threadPoolLifetime: DisposableStack | undefined,
|
||||
) {
|
||||
const { options, fileNames, projectReferences } = config;
|
||||
enableStatisticsAndTracing(sys, options, /*isBuildMode*/ false);
|
||||
@@ -984,7 +998,7 @@ function performIncrementalCompilation(
|
||||
cb(builderProgram);
|
||||
}
|
||||
});
|
||||
threadPool?.stop();
|
||||
dispose(threadPoolLifetime);
|
||||
return sys.exit(exitStatus);
|
||||
}
|
||||
|
||||
@@ -1045,6 +1059,7 @@ function createWatchOfConfigFile(
|
||||
extendedConfigCache: Map<string, ExtendedConfigCacheEntry>,
|
||||
workerThreads: WorkerThreadsHost | undefined,
|
||||
threadPool: ThreadPool | undefined,
|
||||
threadPoolLifetime: DisposableStack | undefined,
|
||||
) {
|
||||
const watchCompilerHost = createWatchCompilerHostOfConfigFile({
|
||||
configFileName: configParseResult.options.configFilePath!,
|
||||
@@ -1059,7 +1074,7 @@ function createWatchOfConfigFile(
|
||||
updateWatchCompilationHost(system, cb, watchCompilerHost);
|
||||
watchCompilerHost.configFileParsingResult = configParseResult;
|
||||
watchCompilerHost.extendedConfigCache = extendedConfigCache;
|
||||
return createWatchProgram(watchCompilerHost);
|
||||
return createWatchProgram(watchCompilerHost, threadPoolLifetime);
|
||||
}
|
||||
|
||||
function createWatchOfFilesAndCompilerOptions(
|
||||
@@ -1071,6 +1086,7 @@ function createWatchOfFilesAndCompilerOptions(
|
||||
watchOptions: WatchOptions | undefined,
|
||||
workerThreads: WorkerThreadsHost | undefined,
|
||||
threadPool: ThreadPool | undefined,
|
||||
threadPoolLifetime: DisposableStack | undefined,
|
||||
) {
|
||||
const watchCompilerHost = createWatchCompilerHostOfFilesAndCompilerOptions({
|
||||
rootFiles,
|
||||
@@ -1083,7 +1099,7 @@ function createWatchOfFilesAndCompilerOptions(
|
||||
threadPool,
|
||||
});
|
||||
updateWatchCompilationHost(system, cb, watchCompilerHost);
|
||||
return createWatchProgram(watchCompilerHost);
|
||||
return createWatchProgram(watchCompilerHost, threadPoolLifetime);
|
||||
}
|
||||
|
||||
interface SolutionPerformance {
|
||||
|
||||
@@ -556,7 +556,6 @@ export function patchHostForBuildInfoWrite<T extends ts.System>(sys: T, version:
|
||||
export class SolutionBuilderHost extends CompilerHost implements ts.SolutionBuilderHost<ts.BuilderProgram> {
|
||||
/** @internal */ declare workerThreads: ts.WorkerThreadsHost | undefined;
|
||||
/** @internal */ declare threadPool: ts.ThreadPool | undefined;
|
||||
/** @internal */ declare membrane: ts.Membrane | undefined;
|
||||
|
||||
createProgram: ts.CreateProgram<ts.BuilderProgram>;
|
||||
|
||||
|
||||
@@ -603,7 +603,7 @@ class TokenOrIdentifierObject implements Node {
|
||||
}
|
||||
|
||||
public getChildren(): Node[] {
|
||||
return this.kind === SyntaxKind.EndOfFileToken ? (this as Node as EndOfFileToken).jsDoc || emptyArray : emptyArray;
|
||||
return this.kind === SyntaxKind.EndOfFileToken ? (this as Node as EndOfFileToken).jsDoc?.slice() || [] : [];
|
||||
}
|
||||
|
||||
public getFirstToken(): Node | undefined {
|
||||
|
||||
@@ -20,7 +20,7 @@ import "./unittests/publicApi";
|
||||
import "./unittests/reuseProgramStructure";
|
||||
import "./unittests/semver";
|
||||
import "./unittests/sharing/hashData";
|
||||
import "./unittests/sharing/resizableArray";
|
||||
import "./unittests/sharing/concurrentMap";
|
||||
import "./unittests/transform";
|
||||
import "./unittests/typeParameterIsPossiblyReferenced";
|
||||
import "./unittests/config/commandLineParsing";
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { ConcurrentMap, wrapStruct } from "../../_namespaces/ts";
|
||||
|
||||
describe("unittests:: sharing:: concurrentMap", () => {
|
||||
it("round trip", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, string>(), ConcurrentMap);
|
||||
assert.equal(map.size(), 0);
|
||||
assert.isFalse(map.has(1));
|
||||
map.set(1, "a");
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
map.delete(1);
|
||||
assert.equal(map.size(), 0);
|
||||
assert.isFalse(map.has(1));
|
||||
});
|
||||
it("large inserts", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
map.insert(i, i);
|
||||
}
|
||||
assert.equal(map.size(), 10_000);
|
||||
});
|
||||
it("large deletes", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
map.insert(i, i);
|
||||
}
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
map.delete(i);
|
||||
}
|
||||
assert.equal(map.size(), 0);
|
||||
});
|
||||
describe("exchange", () => {
|
||||
it("sets key to value if missing", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.exchange(1, 2);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 2);
|
||||
});
|
||||
it("overwrites key if present and value is not undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.exchange(1, 3);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 3);
|
||||
});
|
||||
it("deletes key if present and value is undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.exchange(1, /*value*/ undefined);
|
||||
assert.equal(map.size(), 0);
|
||||
assert.isFalse(map.has(1));
|
||||
assert.equal(map.get(1), /*expected*/ undefined);
|
||||
});
|
||||
it("returns undefined if key was not present", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
const result = map.exchange(1, 2);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
it("returns previous value if key was present", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
const result1 = map.exchange(1, 3);
|
||||
const result2 = map.exchange(1, /*value*/ undefined);
|
||||
assert.equal(result1, 2);
|
||||
assert.equal(result2, 3);
|
||||
});
|
||||
});
|
||||
describe("compareExchange", () => {
|
||||
it("sets key to value if expecting undefined and key not present", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.compareExchange(1, /*expectedValue*/ undefined, 2);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 2);
|
||||
});
|
||||
it("does not set key to value if expecting undefined and key is present", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.compareExchange(1, /*expectedValue*/ undefined, 3);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 2);
|
||||
});
|
||||
it("overwrites key if present, expected value matches, and replacement value is not undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.compareExchange(1, 2, 3);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 3);
|
||||
});
|
||||
it("does not overwrite key if present, expected value does not match, and replacement value is not undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.compareExchange(1, 4, 3);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 2);
|
||||
});
|
||||
it("deletes key if present, expected value matches, and value is undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.compareExchange(1, 2, /*replacementValue*/ undefined);
|
||||
assert.equal(map.size(), 0);
|
||||
assert.isFalse(map.has(1));
|
||||
assert.equal(map.get(1), /*expected*/ undefined);
|
||||
});
|
||||
it("does not delete key if present, expected value does not match, and value is undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
map.compareExchange(1, 3, /*replacementValue*/ undefined);
|
||||
assert.equal(map.size(), 1);
|
||||
assert.isTrue(map.has(1));
|
||||
assert.equal(map.get(1), 2);
|
||||
});
|
||||
it("returns undefined if key was not present and expected value is undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
const result = map.compareExchange(1, /*expectedValue*/ undefined, 2);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
it("returns undefined if key was not present and expected value was not undefined", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
const result = map.compareExchange(1, 2, 3);
|
||||
assert.isUndefined(result);
|
||||
});
|
||||
it("returns previous value if key was present and expected value matches", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
const result1 = map.compareExchange(1, 2, 3);
|
||||
const result2 = map.compareExchange(1, 3, /*value*/ undefined);
|
||||
assert.equal(result1, 2);
|
||||
assert.equal(result2, 3);
|
||||
});
|
||||
it("returns previous value if key was present and expected value did not match", () => {
|
||||
const map = wrapStruct(new ConcurrentMap<number, number>(), ConcurrentMap);
|
||||
map.set(1, 2);
|
||||
const result1 = map.compareExchange(1, 3, 4);
|
||||
const result2 = map.compareExchange(1, 4, /*value*/ undefined);
|
||||
assert.equal(result1, 2);
|
||||
assert.equal(result2, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HashData } from "../../../compiler/sharing/collections/hashData";
|
||||
|
||||
describe("unittests:: hashData", () => {
|
||||
describe("unittests:: sharing:: hashData", () => {
|
||||
describe("findEntryIndex", () => {
|
||||
it("when empty", () => {
|
||||
const hashData = new HashData(0);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Membrane, ProxyResizableArray } from "../../_namespaces/ts";
|
||||
|
||||
describe("unittests:: resizableArray", () => {
|
||||
it("push", () => {
|
||||
const membrane = new Membrane();
|
||||
const array = new ProxyResizableArray(membrane);
|
||||
array.push(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user