@@ -26,10 +26,16 @@ import {
26
26
discoverSnapshotResources ,
27
27
createDiscoveryQueue
28
28
} from './discovery.js' ;
29
+ import Monitoring from '@percy/monitoring' ;
29
30
import { WaitForJob } from './wait-for-job.js' ;
30
31
31
32
const MAX_SUGGESTION_CALLS = 10 ;
32
33
34
+ // If no activity is done for 5 mins, we will stop monitoring
35
+ // system metric eg: (cpu load && memory usage)
36
+ const MONITOR_ACTIVITY_TIMEOUT = 300000 ;
37
+ const MONITORING_INTERVAL_MS = 5000 ; // 5 sec
38
+
33
39
// A Percy instance will create a new build when started, handle snapshot creation, asset discovery,
34
40
// and resource uploads, and will finalize the build when stopped. Snapshots are processed
35
41
// concurrently and the build is not finalized until all snapshots have been handled.
@@ -107,8 +113,15 @@ export class Percy {
107
113
this . browser = new Browser ( this ) ;
108
114
109
115
this . #discovery = createDiscoveryQueue ( this ) ;
116
+ this . discoveryMaxConcurrency = this . #discovery. concurrency ;
110
117
this . #snapshots = createSnapshotsQueue ( this ) ;
111
118
119
+ this . monitoring = new Monitoring ( ) ;
120
+ // used continue monitoring if there is activity going on
121
+ // if there is none, stop it
122
+ this . resetMonitoringId = null ;
123
+ this . monitoringCheckLastExecutedAt = null ;
124
+
112
125
// generator methods are wrapped to autorun and return promises
113
126
for ( let m of [ 'start' , 'stop' , 'flush' , 'idle' , 'snapshot' , 'upload' ] ) {
114
127
// the original generator can be referenced with percy.yield.<method>
@@ -117,6 +130,29 @@ export class Percy {
117
130
}
118
131
}
119
132
133
+ systemMonitoringEnabled ( ) {
134
+ return ( process . env . PERCY_DISABLE_SYSTEM_MONITORING !== 'true' ) ;
135
+ }
136
+
137
+ async configureSystemMonitor ( ) {
138
+ await this . monitoring . startMonitoring ( { interval : MONITORING_INTERVAL_MS } ) ;
139
+ this . resetSystemMonitor ( ) ;
140
+ }
141
+
142
+ // Debouncing logic to only stop Monitoring system
143
+ // if there is no any activity for 5 mins
144
+ // means, no job is pushed in queue from 5 mins
145
+ resetSystemMonitor ( ) {
146
+ if ( this . resetMonitoringId ) {
147
+ clearTimeout ( this . resetMonitoringId ) ;
148
+ this . resetMonitoringId = null ;
149
+ }
150
+
151
+ this . resetMonitoringId = setTimeout ( ( ) => {
152
+ this . monitoring . stopMonitoring ( ) ;
153
+ } , MONITOR_ACTIVITY_TIMEOUT ) ;
154
+ }
155
+
120
156
// Shortcut for controlling the global logger's log level.
121
157
loglevel ( level ) {
122
158
return logger . loglevel ( level ) ;
@@ -169,6 +205,13 @@ export class Percy {
169
205
this . cliStartTime = new Date ( ) . toISOString ( ) ;
170
206
171
207
try {
208
+ // started monitoring system metrics
209
+
210
+ if ( this . systemMonitoringEnabled ( ) ) {
211
+ await this . configureSystemMonitor ( ) ;
212
+ await this . monitoring . logSystemInfo ( ) ;
213
+ }
214
+
172
215
if ( process . env . PERCY_CLIENT_ERROR_LOGS !== 'false' ) {
173
216
this . log . warn ( 'Notice: Percy collects CI logs to improve service and enhance your experience. These logs help us debug issues and provide insights on your dashboards, making it easier to optimize the product experience. Logs are stored securely for 30 days. You can opt out anytime with export PERCY_CLIENT_ERROR_LOGS=false, but keeping this enabled helps us offer the best support and features.' ) ;
174
217
}
@@ -298,13 +341,66 @@ export class Percy {
298
341
this . log . error ( err ) ;
299
342
throw err ;
300
343
} finally {
344
+ // stop monitoring system metric, if not already stopped
345
+ this . monitoring . stopMonitoring ( ) ;
346
+ clearTimeout ( this . resetMonitoringId ) ;
347
+
301
348
// This issue doesn't comes under regular error logs,
302
349
// it's detected if we just and stop percy server
303
350
await this . checkForNoSnapshotCommandError ( ) ;
304
351
await this . sendBuildLogs ( ) ;
305
352
}
306
353
}
307
354
355
+ checkAndUpdateConcurrency ( ) {
356
+ // early exit if monitoring is disabled
357
+ if ( ! this . systemMonitoringEnabled ( ) ) return ;
358
+
359
+ // early exit if asset discovery concurrency change is disabled
360
+ // NOTE: system monitoring will still be running as only concurrency
361
+ // change is disabled
362
+ if ( process . env . PERCY_DISABLE_CONCURRENCY_CHANGE === 'true' ) return ;
363
+
364
+ // start system monitoring if not already doing...
365
+ // this doesn't handle cases where there is suggest cpu spikes
366
+ // in less 1 sec range and if monitoring is not in running state
367
+ if ( this . monitoringCheckLastExecutedAt && Date . now ( ) - this . monitoringCheckLastExecutedAt < MONITORING_INTERVAL_MS ) return ;
368
+
369
+ if ( ! this . monitoring . running ) this . configureSystemMonitor ( ) ;
370
+ else this . resetSystemMonitor ( ) ;
371
+
372
+ // early return if last executed was less than 5 seconds
373
+ // as we will get the same cpu/mem info under 5 sec interval
374
+ const { cpuInfo, memoryUsageInfo } = this . monitoring . getMonitoringInfo ( ) ;
375
+ this . log . debug ( `cpuInfo: ${ JSON . stringify ( cpuInfo ) } ` ) ;
376
+ this . log . debug ( `memoryInfo: ${ JSON . stringify ( memoryUsageInfo ) } ` ) ;
377
+
378
+ if ( cpuInfo . currentUsagePercent >= 80 || memoryUsageInfo . currentUsagePercent >= 80 ) {
379
+ let currentConcurrent = this . #discovery. concurrency ;
380
+
381
+ // concurrency must be betweeen [1, (default/user defined value)]
382
+ let newConcurrency = Math . max ( 1 , parseInt ( currentConcurrent / 2 ) ) ;
383
+ newConcurrency = Math . min ( this . discoveryMaxConcurrency , newConcurrency ) ;
384
+
385
+ this . log . debug ( `Downscaling discovery browser concurrency from ${ this . #discovery. concurrency } to ${ newConcurrency } ` ) ;
386
+ this . #discovery. set ( { concurrency : newConcurrency } ) ;
387
+ } else if ( cpuInfo . currentUsagePercent <= 50 && memoryUsageInfo . currentUsagePercent <= 50 ) {
388
+ let currentConcurrent = this . #discovery. concurrency ;
389
+ let newConcurrency = currentConcurrent + 2 ;
390
+
391
+ // concurrency must be betweeen [1, (default/user-defined value)]
392
+ newConcurrency = Math . min ( this . discoveryMaxConcurrency , newConcurrency ) ;
393
+ newConcurrency = Math . max ( 1 , newConcurrency ) ;
394
+
395
+ this . log . debug ( `Upscaling discovery browser concurrency from ${ this . #discovery. concurrency } to ${ newConcurrency } ` ) ;
396
+ this . #discovery. set ( { concurrency : newConcurrency } ) ;
397
+ }
398
+
399
+ // reset timeout to stop monitoring after no-activity of 5 mins
400
+ this . resetSystemMonitor ( ) ;
401
+ this . monitoringCheckLastExecutedAt = Date . now ( ) ;
402
+ }
403
+
308
404
// Takes one or more snapshots of a page while discovering resources to upload with the resulting
309
405
// snapshots. Once asset discovery has completed for the provided snapshots, the queued task will
310
406
// resolve and an upload task will be queued separately.
@@ -351,7 +447,7 @@ export class Percy {
351
447
yield * discoverSnapshotResources ( this . #discovery, {
352
448
skipDiscovery : this . skipDiscovery ,
353
449
dryRun : this . dryRun ,
354
-
450
+ checkAndUpdateConcurrency : this . checkAndUpdateConcurrency . bind ( this ) ,
355
451
snapshots : yield * gatherSnapshots ( options , {
356
452
meta : { build : this . build } ,
357
453
config : this . config
0 commit comments