tsgo를 실행하면, 기가바이트 단위의 메모리를 사용하는 것을 보는 게 드문 일은 아닙니다.tsgo는 스레드당 하나의 타입 체커(Type checker)를 생성합니다.tsgo를 실행하면, 이러한 타입 생성 패턴이 복합적으로 작용하여 대량의 중복되거나 사용되지 않는 메모리를 발생시킵니다.tsgo를 실행하겠습니다. node_modules를 포함하여 약 7,000개의 .ts 파일이 있습니다.-inuse_space 플래그를 사용하여 어떤 함수가 메모리를 가장 많이 할당했는지 확인해 보겠습니다.Total live heap: 1471.9 MB pprof writer self-overhead: 75.2 MB real live data: 1321.5 MB MB pct Family────────────────────────────────────────────────────────────────────────────── 594.72 45.0% AST arenas (parser-allocated) 399.12 30.2% Checker (type/signature computation) 121.79 9.2% LinkStore (per-node/per-symbol caches) 63.38 4.8% OS / syscall / file I/O 62.58 4.7% Binder (symbol/flow declarations) 22.33 1.7% Parser (intern maps, etc.) 20.24 1.5% pkg: collections 15.54 1.2% Checker arenas 13.46 1.0% AST utilities 6.58 0.5% Compiler / module resolution 1.10 0.1% pkg: core 0.70 0.1% pkg: packagejsonChecker 구조체)에 의해 할당된 메모리입니다.tsgo를 --singleThreaded 옵션으로 실행하면 어떻게 될까요?Total live heap: 797.4 MB pprof writer self-overhead: 3.6 MB real live data: 790.2 MB MB pct Family────────────────────────────────────────────────────────────────────────────── 522.95 66.2% AST arenas (parser-allocated) 63.37 8.0% OS / syscall / file I/O 62.63 7.9% Binder (symbol/flow declarations) 51.93 6.6% Checker (type/signature computation) 23.01 2.9% LinkStore (per-node/per-symbol caches) 22.51 2.8% Parser (intern maps, etc.) 16.78 2.1% AST utilities 16.15 2.0% pkg: collections 10.21 1.3% Compiler / module resolution 0.58 0.1% pkg: packagejson 0.10 0.0% pkg: core 0.01 0.0% ** unclassified **Checker 풀을 생성하는 것입니다.func newCheckerPoolWithTracing(program *Program, tr *tracing.Tracing) *checkerPool { checkerCount := 4 if program.SingleThreaded() { checkerCount = 1 } else if c := program.Options().Checkers; c != nil { checkerCount = *c } checkerCount = max(min(checkerCount, len(program.files), 256), 1) pool := &checkerPool{ program: program, checkers: make([]*checker.Checker, checkerCount), locks: make([]*sync.Mutex, checkerCount), tracing: tr, } return pool}Checker가 생성될 때, 전체 TypeScript 프로그램 AST와 모든 파일이 제공됩니다.func NewChecker(program Program, tracer *Tracer) (*Checker, *sync.Mutex) { program.BindSourceFiles() c := &Checker{} c.id = nextCheckerID.Add(1) c.tracer = tracer c.program = program c.compilerOptions = program.Options() c.files = program.SourceFiles() c.fileIndexMap = createFileIndexMap(c.files) // ... 추가 코드}Checker에 할당됩니다.Checker는 타입 체크를 위한 고유한 상태를 가집니다(나중에 더 자세히 살펴보겠습니다). 다음은 중복 작업의 예시입니다:a.ts 파일이 Checker 1로 가서 수많은 타입을 생성합니다.b.ts 파일이 a.ts에서 일부 타입을 임포트(Import)하고 Checker 2로 갑니다.Checker 2는 별도의 상태를 가지고 있으므로, a.ts에 대한 데이터를 다시 계산하고 다시 할당해야 합니다.pprof 실행 결과에서 가장 많은 할당을 기록한 Checker 함수들은 다음과 같았습니다:Checker.newSymbol() (심볼)Checker.newObjectType() (타입)Checker.instantiateType() (타입)Checker에서 정확히 어떤 데이터가 할당되고 있는지 살펴보겠습니다.Checker는 생성될 수 있는 수많은 타입을 저장하기 위한 많은 저장소(Store)를 가지고 있습니다.type Checker struct { stringLiteralTypes map[string]*Type numberLiteralTypes map[jsnum.Number]*Type bigintLiteralTypes map[jsnum.PseudoBigInt]*Type enumLiteralTypes map[EnumLiteralKey]*Type indexedAccessTypes map[CacheHashKey]*Type templateLiteralTypes map[CacheHashKey]*Type stringMappingTypes map[StringMappingKey]*Type cachedTypes map[CachedTypeKey]*Type cachedSignatures map[CachedSignatureKey]*Signature narrowedTypes map[NarrowedTypeKey]*Type assignmentReducedTypes map[AssignmentReducedKey]*Type discriminatedContextualTypes map[DiscriminatedContextualTypeKey]*Type instantiationExpressionTypes map[InstantiationExpressionKey]*Type substitutionTypes map[SubstitutionTypeKey]*Type reverseMappedCache map[ReverseMappedTypeKey]*Type reverseHomomorphicMappedCache map[ReverseMappedTypeKey]*Type iterationTypesCache map[IterationTypesKey]IterationTypes tupleTypes map[CacheHashKey]*Type unionTypes map[CacheHashKey]*Type unionOfUnionTypes map[UnionOfUnionKey]*Type intersectionTypes map[CacheHashKey]*Type propertiesTypes map[PropertiesTypesKey]*Type flowLoopCache map[FlowLoopKey]*Type flowTypeCache map[*ast.Node]*Type errorTypes map[CacheHashKey]*Type // 그 외 다수!}Checker에 속하며 데이터 공유는 없습니다.type BuildTuple<L extends number, T extends any[] = []> = T['length'] extends L ? T : BuildTuple<L, [...T, any]>;type TC = BuildTuple<100>;declare const x: TC;export const c0 = x[0];export const cLen: 100 = x.length;BuildTuple<L, T> 타입은 빈 튜플 타입 []부터 100개의 any가 들어있는 튜플([any, any, ... any])까지 재귀적으로 튜플 타입을 생성합니다.tsgo로 실행하면, 100개의 튜플 타입이 생성되어 4개의 타입 체커에 중복되어 나타나야 합니다(또한 100개의 숫자 리터럴 타입도 마찬가지입니다). 단일 체커 4개 체커 ───────────────── ───────────────────────────── tupleTypes 102 [102 102 102 102] → 408 numberLiteralTypes 101 [101 101 101 101] → 404Checker는 수많은 심볼을 저장합니다.type Checker struct { // ... 추가 코드 symbolArena core.Arena[ast.Symbol] // ... 추가 코드}tsgo를 수정했습니다.tsgo --checkers 4심볼 | 종류 | 개수 |
|---|---|---|
at | Method | 34,500 |
_ | Property | 25,600 |
name | Property | 24,700 |
value | FuncVar | 22,800 |
@@iterator | Method | 22,300 |
data | Property | 22,100 |
enumValues | Property | 21,900 |
columnType | Property | 21,000 |
dataType | Property | 21,000 |
generated | Property | 19,500 |
at 심볼 개수를 살펴봅시다. 만약 싱글 스레드 tsgo에서 이 수치가 감소한다면, 다른 스레드들이 이를 복제하고 있다는 뜻일 것입니다.tsgo --checkers 1심볼 | 종류 | 개수 |
|---|---|---|
props | FuncVar | 16,800 |
at | Method | 14,600 |
children | Property | 10,400 |
value | FuncVar | 10,200 |
@@iterator | Method | 9,500 |
className | Property | 9,200 |
data | Property | 8,500 |
forEach | Method | 8,100 |
map | Method | 8,000 |
find | Method | 7,900 |
tsgo를 실행했을 때 약 2만 개나 더 많은 at 심볼이 생성되었습니다!at 심볼은 Array<T>.prototype.at에서 옵니다. Array<T>를 생성하고 그에 대한 속성 조회를 수행함으로써 TypeScript가 이 심볼을 생성하도록 강제할 수 있습니다. 이렇게 하면 TypeScript가 Array 객체의 모든 멤버를 확인(Resolve)하고 해당 심볼들을 생성하게 됩니다. [3]declare const arr: Array<string>;export const len = arr.length;--checkers 4 옵션으로 tsgo를 실행하면 각 파일이 하나의 Checker로 할당될 것이고, at 심볼이 중복되는지 확인할 수 있습니다. --checkers 1 --checkers 4 ───────────── ────────────────────────────── 합계 합계 c0 c1 c2 c3at 1 4 1 1 1 1Array<string>.prototype.at에 대한 심볼을 중복 생성했습니다.Array<string>, Array<number> 등은 모두 at 및 다른 멤버들에 대해 각자의 심볼을 갖게 됩니다. 이는 지극히 정상적이고 표준적인 동작입니다.tsgo가 다른 스레드에서 얼마나 쉽게 많은 심볼을 중복 생성할 수 있는지 알 수 있습니다.type MyDataStructure<T> = { field1: T; field2: string; // ... field100: string;}Checker에서 이 타입이 조회되고 중복될 가능성이 매우 높습니다.ZodObject를 반환합니다.const emailSchema = z.string().email().min(5).max(120).toLowerCase();.string(), .email() 등은 새로운 ZodObject<Shape, Config> 타입을 인스턴스화하고, 속성 체이닝은 TypeScript가 심볼을 확인하고 생성하게 만듭니다(개별 타입 할당은 물론이고요!).tsgo 소스 코드를 깊이 파고드는 즐거운 시간이었습니다.값의 해시 -> (스레드, 인덱스)를 매핑하는 맵).아직 댓글이 없습니다.
첫 번째 댓글을 작성해보세요!