@@ -14,6 +14,14 @@ import {ImportGraph} from './imports';
14
14
* Analyzes a `ts.Program` for cycles.
15
15
*/
16
16
export class CycleAnalyzer {
17
+ /**
18
+ * Cycle detection is requested with the same `from` source file for all used directives and pipes
19
+ * within a component, which makes it beneficial to cache the results as long as the `from` source
20
+ * file has not changed. This avoids visiting the import graph that is reachable from multiple
21
+ * directives/pipes more than once.
22
+ */
23
+ private cachedResults : CycleResults | null = null ;
24
+
17
25
constructor ( private importGraph : ImportGraph ) { }
18
26
19
27
/**
@@ -24,10 +32,13 @@ export class CycleAnalyzer {
24
32
* otherwise.
25
33
*/
26
34
wouldCreateCycle ( from : ts . SourceFile , to : ts . SourceFile ) : Cycle | null {
35
+ // Try to reuse the cached results as long as the `from` source file is the same.
36
+ if ( this . cachedResults === null || this . cachedResults . from !== from ) {
37
+ this . cachedResults = new CycleResults ( from , this . importGraph ) ;
38
+ }
39
+
27
40
// Import of 'from' -> 'to' is illegal if an edge 'to' -> 'from' already exists.
28
- return this . importGraph . transitiveImportsOf ( to ) . has ( from ) ?
29
- new Cycle ( this . importGraph , from , to ) :
30
- null ;
41
+ return this . cachedResults . wouldBeCyclic ( to ) ? new Cycle ( this . importGraph , from , to ) : null ;
31
42
}
32
43
33
44
/**
@@ -37,10 +48,83 @@ export class CycleAnalyzer {
37
48
* import graph for cycle creation.
38
49
*/
39
50
recordSyntheticImport ( from : ts . SourceFile , to : ts . SourceFile ) : void {
51
+ this . cachedResults = null ;
40
52
this . importGraph . addSyntheticImport ( from , to ) ;
41
53
}
42
54
}
43
55
56
+ const NgCyclicResult = Symbol ( 'NgCyclicResult' ) ;
57
+ type CyclicResultMarker = {
58
+ __brand : 'CyclicResultMarker' ;
59
+ } ;
60
+ type CyclicSourceFile = ts . SourceFile & { [ NgCyclicResult ] ?: CyclicResultMarker } ;
61
+
62
+ /**
63
+ * Stores the results of cycle detection in a memory efficient manner. A symbol is attached to
64
+ * source files that indicate what the cyclic analysis result is, as indicated by two markers that
65
+ * are unique to this instance. This alleviates memory pressure in large import graphs, as each
66
+ * execution is able to store its results in the same memory location (i.e. in the symbol
67
+ * on the source file) as earlier executions.
68
+ */
69
+ class CycleResults {
70
+ private readonly cyclic = { } as CyclicResultMarker ;
71
+ private readonly acyclic = { } as CyclicResultMarker ;
72
+
73
+ constructor ( readonly from : ts . SourceFile , private importGraph : ImportGraph ) { }
74
+
75
+ wouldBeCyclic ( sf : ts . SourceFile ) : boolean {
76
+ const cached = this . getCachedResult ( sf ) ;
77
+ if ( cached !== null ) {
78
+ // The result for this source file has already been computed, so return its result.
79
+ return cached ;
80
+ }
81
+
82
+ if ( sf === this . from ) {
83
+ // We have reached the source file that we want to create an import from, which means that
84
+ // doing so would create a cycle.
85
+ return true ;
86
+ }
87
+
88
+ // Assume for now that the file will be acyclic; this prevents infinite recursion in the case
89
+ // that `sf` is visited again as part of an existing cycle in the graph.
90
+ this . markAcyclic ( sf ) ;
91
+
92
+ const imports = this . importGraph . importsOf ( sf ) ;
93
+ for ( const imported of imports ) {
94
+ if ( this . wouldBeCyclic ( imported ) ) {
95
+ this . markCyclic ( sf ) ;
96
+ return true ;
97
+ }
98
+ }
99
+ return false ;
100
+ }
101
+
102
+ /**
103
+ * Returns whether the source file is already known to be cyclic, or `null` if the result is not
104
+ * yet known.
105
+ */
106
+ private getCachedResult ( sf : CyclicSourceFile ) : boolean | null {
107
+ const result = sf [ NgCyclicResult ] ;
108
+ if ( result === this . cyclic ) {
109
+ return true ;
110
+ } else if ( result === this . acyclic ) {
111
+ return false ;
112
+ } else {
113
+ // Either the symbol is missing or its value does not correspond with one of the current
114
+ // result markers. As such, the result is unknown.
115
+ return null ;
116
+ }
117
+ }
118
+
119
+ private markCyclic ( sf : CyclicSourceFile ) : void {
120
+ sf [ NgCyclicResult ] = this . cyclic ;
121
+ }
122
+
123
+ private markAcyclic ( sf : CyclicSourceFile ) : void {
124
+ sf [ NgCyclicResult ] = this . acyclic ;
125
+ }
126
+ }
127
+
44
128
/**
45
129
* Represents an import cycle between `from` and `to` in the program.
46
130
*
0 commit comments