@@ -5,9 +5,7 @@ namespace Microsoft.ComponentDetection.Detectors.Helm;
55using System . Collections . Concurrent ;
66using System . Collections . Generic ;
77using System . IO ;
8- using System . Linq ;
98using System . Reactive . Linq ;
10- using System . Reactive . Threading . Tasks ;
119using System . Threading ;
1210using System . Threading . Tasks ;
1311using Microsoft . ComponentDetection . Common ;
@@ -51,31 +49,97 @@ public override async Task<IndividualDetectorScanResult> ExecuteDetectorAsync(Sc
5149 return await base . ExecuteDetectorAsync ( request , cancellationToken ) ;
5250 }
5351
54- protected override async Task < IObservable < ProcessRequest > > OnPrepareDetectionAsync (
52+ /// <summary>
53+ /// Pre-filters scan work to values files that are co-located with a Helm chart.
54+ ///
55+ /// This is intentionally implemented as a streaming pipeline (instead of
56+ /// materializing all matching files with ToList) to reduce peak memory usage and
57+ /// start emitting work earlier on large repositories.
58+ ///
59+ /// Enumeration order is not guaranteed, so values files may be seen before the
60+ /// corresponding Chart.yaml. To preserve correctness, values files are buffered
61+ /// per directory until a chart file for that directory is observed, then released.
62+ /// </summary>
63+ /// <returns>
64+ /// A prepared observable that emits only values-file requests belonging to
65+ /// directories that contain a Chart.yaml/Chart.yml.
66+ /// </returns>
67+ protected override Task < IObservable < ProcessRequest > > OnPrepareDetectionAsync (
5568 IObservable < ProcessRequest > processRequests ,
5669 IDictionary < string , string > detectorArgs ,
5770 CancellationToken cancellationToken = default )
5871 {
59- // Materialize all matching files first so that chart directories are fully
60- // known before any values file is decided on, regardless of enumeration order.
61- var allRequests = await processRequests . ToList ( ) . ToTask ( cancellationToken ) ;
62-
63- // Pass 1: record every directory that contains a Chart.yaml / Chart.yml.
64- foreach ( var request in allRequests )
72+ return Task . FromResult ( Observable . Create < ProcessRequest > ( observer =>
6573 {
66- if ( IsChartFile ( Path . GetFileName ( request . ComponentStream . Location ) ) )
74+ // Buffer only values files whose directory has not yet produced a Chart file.
75+ var pendingValuesByDirectory = new Dictionary < string , List < ProcessRequest > > ( StringComparer . OrdinalIgnoreCase ) ;
76+ var gate = new object ( ) ;
77+
78+ var subscription = processRequests . Subscribe (
79+ request =>
80+ {
81+ var fileName = Path . GetFileName ( request . ComponentStream . Location ) ;
82+ var directory = Path . GetDirectoryName ( request . ComponentStream . Location ) ?? string . Empty ;
83+
84+ // Protect shared state because IObservable callbacks may arrive concurrently.
85+ lock ( gate )
86+ {
87+ if ( IsChartFile ( fileName ) )
88+ {
89+ // Mark this directory as a Helm chart directory.
90+ this . helmChartDirectories . TryAdd ( directory , true ) ;
91+
92+ // Release any values files that arrived earlier for this directory.
93+ if ( pendingValuesByDirectory . Remove ( directory , out var pendingRequests ) )
94+ {
95+ foreach ( var pendingRequest in pendingRequests )
96+ {
97+ observer . OnNext ( pendingRequest ) ;
98+ }
99+ }
100+
101+ return ;
102+ }
103+
104+ if ( ! IsValuesFile ( fileName ) )
105+ {
106+ // Ignore non-values files (Chart files are handled above).
107+ return ;
108+ }
109+
110+ if ( this . helmChartDirectories . ContainsKey ( directory ) )
111+ {
112+ // Directory is already known to be a chart; emit immediately.
113+ observer . OnNext ( request ) ;
114+ return ;
115+ }
116+
117+ // Chart file not seen yet for this directory; queue for later release.
118+ if ( ! pendingValuesByDirectory . TryGetValue ( directory , out var pendingRequestsForDirectory ) )
119+ {
120+ pendingRequestsForDirectory = [ ] ;
121+ pendingValuesByDirectory [ directory ] = pendingRequestsForDirectory ;
122+ }
123+
124+ pendingRequestsForDirectory . Add ( request ) ;
125+ }
126+ } ,
127+ observer . OnError ,
128+ observer . OnCompleted ) ;
129+
130+ var cancellationRegistration = cancellationToken . Register ( ( ) =>
67131 {
68- this . helmChartDirectories . TryAdd (
69- Path . GetDirectoryName ( request . ComponentStream . Location ) ! , true ) ;
70- }
71- }
132+ // Stop forwarding events if detection is cancelled.
133+ subscription . Dispose ( ) ;
134+ observer . OnCompleted ( ) ;
135+ } ) ;
72136
73- // Pass 2: emit only the values files that sit in a known chart directory.
74- return allRequests
75- . Where ( r =>
76- IsValuesFile ( Path . GetFileName ( r . ComponentStream . Location ) ) &&
77- this . helmChartDirectories . ContainsKey ( Path . GetDirectoryName ( r . ComponentStream . Location ) ! ) )
78- . ToObservable ( ) ;
137+ return ( ) =>
138+ {
139+ cancellationRegistration . Dispose ( ) ;
140+ subscription . Dispose ( ) ;
141+ } ;
142+ } ) ) ;
79143 }
80144
81145 protected override async Task OnFileFoundAsync ( ProcessRequest processRequest , IDictionary < string , string > detectorArgs , CancellationToken cancellationToken = default )
0 commit comments