Add port of ConcurrentMap from .NET

This commit is contained in:
Ron Buckton
2023-09-22 13:56:37 -04:00
parent 7fa1fd0f36
commit f209dfcedd
50 changed files with 2541 additions and 1122 deletions
+1 -1
View File
@@ -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} */
+20 -18
View File
@@ -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
View File
@@ -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]();
}
+2 -2
View File
@@ -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) {
+7 -5
View File
@@ -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;
+44
View File
@@ -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
View File
@@ -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;
}
}
+127
View File
@@ -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;
}
+126
View File
@@ -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));
}
}
}
+37
View File
@@ -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);
}
}
+6 -31
View File
@@ -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;
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+5 -8
View File
@@ -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;
+4 -4
View File
@@ -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>;
}
+65
View File
@@ -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>;
}
+106 -1
View File
@@ -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;
}
+9 -9
View File
@@ -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 {
+1 -1
View File
@@ -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);
+2 -6
View File
@@ -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;
}
+5 -1
View File
@@ -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);
}
}
+5 -8
View File
@@ -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;
}
+24 -38
View File
@@ -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);
}
+93
View File
@@ -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);
}
};
}
+102
View File
@@ -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));
// }
// }
+5 -11
View File
@@ -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;
}
+2 -5
View File
@@ -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);
}
}
+15
View File
@@ -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);
}
+31
View File
@@ -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;
}
}
+262 -125
View File
@@ -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));
};
}
}
+6 -20
View File
@@ -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;
}
+12
View File
@@ -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) {
+14 -6
View File
@@ -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 -1
View File
@@ -2,7 +2,8 @@
"extends": "../tsconfig-base",
"compilerOptions": {
"types": ["node"],
"experimentalDecorators": true
"experimentalDecorators": true,
"strictBindCallApply": true
},
"references": [
+3 -4
View File
@@ -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
View File
@@ -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);
};
}
}
/**
+8 -2
View File
@@ -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 => {
+23 -22
View File
@@ -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
View File
@@ -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);
}
+36 -20
View File
@@ -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 {
-1
View File
@@ -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>;
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 -1
View File
@@ -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);
});
});