77
88import { describe , it , expect , beforeEach , vi , afterEach } from 'vitest' ;
99import * as fc from 'fast-check' ;
10+ import {
11+ FPSMonitor ,
12+ StateDebouncer ,
13+ VisibilityOptimizer
14+ } from '../../core/performance/performanceMonitor' ;
15+
16+ // Mock useDigitalHumanStore
17+ vi . mock ( '../../store/digitalHumanStore' , ( ) => ( {
18+ useDigitalHumanStore : {
19+ getState : ( ) => ( {
20+ updatePerformanceMetrics : vi . fn ( ) ,
21+ } ) ,
22+ } ,
23+ } ) ) ;
1024
1125describe ( 'Performance Properties' , ( ) => {
1226 beforeEach ( ( ) => {
@@ -23,122 +37,194 @@ describe('Performance Properties', () => {
2337 * Property 43: 3D Viewer Frame Rate
2438 * For any normal operation period of 10 seconds, the 3D_Viewer SHALL maintain
2539 * an average frame rate of at least 30 FPS.
40+ *
41+ * **Validates: Requirements 12.1**
2642 */
27- it ( 'Property 43: 3D Viewer Frame Rate - maintains minimum 30 FPS' , async ( ) => {
28- await fc . assert (
29- fc . asyncProperty (
30- fc . integer ( { min : 30 , max : 120 } ) , // Target FPS
31- fc . integer ( { min : 5 , max : 15 } ) , // Duration in seconds
32- async ( targetFps , durationSeconds ) => {
33- const frames : number [ ] = [ ] ;
34- const startTime = Date . now ( ) ;
35- const frameInterval = 1000 / targetFps ;
36-
37- // Simulate frame rendering
38- let currentTime = startTime ;
39- while ( currentTime - startTime < durationSeconds * 1000 ) {
40- frames . push ( currentTime ) ;
41- currentTime += frameInterval + Math . random ( ) * 5 ; // Add some jitter
43+ describe ( 'Property 43: 3D Viewer Frame Rate' , ( ) => {
44+ it ( 'maintains minimum 30 FPS during normal operation' , async ( ) => {
45+ await fc . assert (
46+ fc . asyncProperty (
47+ fc . integer ( { min : 30 , max : 120 } ) , // Target FPS
48+ fc . integer ( { min : 5 , max : 15 } ) , // Duration in seconds
49+ async ( targetFps , durationSeconds ) => {
50+ const frames : number [ ] = [ ] ;
51+ const startTime = Date . now ( ) ;
52+ const frameInterval = 1000 / targetFps ;
53+
54+ // Simulate frame rendering
55+ let currentTime = startTime ;
56+ while ( currentTime - startTime < durationSeconds * 1000 ) {
57+ frames . push ( currentTime ) ;
58+ currentTime += frameInterval + Math . random ( ) * 5 ; // Add some jitter
59+ }
60+
61+ // Calculate average FPS
62+ const totalTime = ( frames [ frames . length - 1 ] - frames [ 0 ] ) / 1000 ;
63+ const averageFps = frames . length / totalTime ;
64+
65+ // Should maintain at least 30 FPS (with some tolerance for jitter)
66+ expect ( averageFps ) . toBeGreaterThanOrEqual ( 25 ) ; // Allow some tolerance
4267 }
68+ ) ,
69+ { numRuns : 100 }
70+ ) ;
71+ } ) ;
4372
44- // Calculate average FPS
45- const totalTime = ( frames [ frames . length - 1 ] - frames [ 0 ] ) / 1000 ;
46- const averageFps = frames . length / totalTime ;
73+ it ( 'FPSMonitor correctly calculates frame rate' , ( ) => {
74+ const monitor = new FPSMonitor ( { sampleSize : 60 , targetFPS : 60 , warningThreshold : 30 } ) ;
4775
48- // Should maintain at least 30 FPS (with some tolerance for jitter)
49- expect ( averageFps ) . toBeGreaterThanOrEqual ( 25 ) ; // Allow some tolerance
50- }
51- ) ,
52- { numRuns : 100 }
53- ) ;
76+ // Initially should be 0
77+ expect ( monitor . getCurrentFPS ( ) ) . toBe ( 0 ) ;
78+
79+ // After stopping, should still return last calculated value
80+ monitor . stop ( ) ;
81+ expect ( monitor . getCurrentFPS ( ) ) . toBeGreaterThanOrEqual ( 0 ) ;
82+ } ) ;
5483 } ) ;
5584
5685 /**
5786 * Property 44: State Update Debounce
5887 * For any sequence of rapid state updates (more than 10 per second),
5988 * the system SHALL debounce to prevent more than 10 re-renders per second.
89+ *
90+ * **Validates: Requirements 12.4**
6091 */
61- it ( 'Property 44: State Update Debounce - limits re-renders to 10/second' , async ( ) => {
62- await fc . assert (
63- fc . asyncProperty (
64- fc . integer ( { min : 20 , max : 100 } ) , // Number of rapid updates
65- async ( updateCount ) => {
66- const maxRendersPerSecond = 10 ;
67- const debounceInterval = 1000 / maxRendersPerSecond ; // 100ms
68-
69- let lastRenderTime = 0 ;
70- let renderCount = 0 ;
71- const renders : number [ ] = [ ] ;
72-
73- // Simulate rapid state updates
74- for ( let i = 0 ; i < updateCount ; i ++ ) {
75- const updateTime = i * 10 ; // 10ms apart (100 updates/second)
76-
77- // Debounce logic
78- if ( updateTime - lastRenderTime >= debounceInterval ) {
79- renderCount ++ ;
80- renders . push ( updateTime ) ;
81- lastRenderTime = updateTime ;
92+ describe ( 'Property 44: State Update Debounce' , ( ) => {
93+ it ( 'limits re-renders to configured maximum per second' , async ( ) => {
94+ await fc . assert (
95+ fc . asyncProperty (
96+ fc . integer ( { min : 20 , max : 100 } ) , // Number of rapid updates
97+ fc . integer ( { min : 5 , max : 20 } ) , // Max updates per second
98+ async ( updateCount , maxUpdatesPerSecond ) => {
99+ const debouncer = new StateDebouncer ( {
100+ maxUpdatesPerSecond,
101+ debounceInterval : 1000 / maxUpdatesPerSecond ,
102+ } ) ;
103+
104+ let executionCount = 0 ;
105+ const updateFn = ( ) => { executionCount ++ ; } ;
106+
107+ // Simulate rapid updates
108+ for ( let i = 0 ; i < updateCount ; i ++ ) {
109+ debouncer . debounce ( updateFn ) ;
82110 }
111+
112+ // Immediate executions should be limited
113+ expect ( executionCount ) . toBeLessThanOrEqual ( maxUpdatesPerSecond + 1 ) ;
114+
115+ debouncer . clear ( ) ;
83116 }
117+ ) ,
118+ { numRuns : 100 }
119+ ) ;
120+ } ) ;
84121
85- // Calculate renders per second
86- const totalTime = ( updateCount * 10 ) / 1000 ; // in seconds
87- const rendersPerSecond = renderCount / totalTime ;
122+ it ( 'StateDebouncer respects debounce interval' , ( ) => {
123+ const debouncer = new StateDebouncer ( {
124+ maxUpdatesPerSecond : 10 ,
125+ debounceInterval : 100 ,
126+ } ) ;
88127
89- // Should not exceed 10 renders per second
90- expect ( rendersPerSecond ) . toBeLessThanOrEqual ( maxRendersPerSecond + 1 ) ; // Allow small tolerance
91- }
92- ) ,
93- { numRuns : 100 }
94- ) ;
128+ let count = 0 ;
129+ const updateFn = ( ) => { count ++ ; } ;
130+
131+ // First call should execute immediately
132+ debouncer . debounce ( updateFn ) ;
133+ expect ( count ) . toBe ( 1 ) ;
134+
135+ // Rapid subsequent calls should be debounced
136+ debouncer . debounce ( updateFn ) ;
137+ debouncer . debounce ( updateFn ) ;
138+ debouncer . debounce ( updateFn ) ;
139+
140+ // Only one more should have executed (or be pending)
141+ expect ( count ) . toBeLessThanOrEqual ( 2 ) ;
142+
143+ debouncer . clear ( ) ;
144+ } ) ;
95145 } ) ;
96146
97147 /**
98148 * Property 45: Page Visibility Optimization
99149 * For any page visibility change to 'hidden', the system SHALL pause
100150 * non-essential processing within 1 second.
151+ *
152+ * **Validates: Requirements 12.5**
101153 */
102- it ( 'Property 45: Page Visibility Optimization - pauses on hidden' , async ( ) => {
103- await fc . assert (
104- fc . asyncProperty (
105- fc . boolean ( ) , // Is page visible
106- async ( isVisible ) => {
107- let processingPaused = false ;
108- let pauseTime : number | null = null ;
109- const visibilityChangeTime = Date . now ( ) ;
110-
111- // Simulate visibility change handler
112- const handleVisibilityChange = ( visible : boolean ) => {
113- if ( ! visible ) {
114- // Pause non-essential processing
115- processingPaused = true ;
116- pauseTime = Date . now ( ) ;
117- } else {
118- processingPaused = false ;
119- pauseTime = null ;
120- }
121- } ;
154+ describe ( 'Property 45: Page Visibility Optimization' , ( ) => {
155+ it ( 'pauses processing when page becomes hidden' , async ( ) => {
156+ await fc . assert (
157+ fc . asyncProperty (
158+ fc . boolean ( ) , // Is page visible
159+ fc . integer ( { min : 50 , max : 500 } ) , // Pause delay
160+ async ( isVisible , pauseDelay ) => {
161+ let processingPaused = false ;
162+ let pauseTime : number | null = null ;
163+ const visibilityChangeTime = Date . now ( ) ;
164+
165+ // Simulate visibility change handler
166+ const handleVisibilityChange = ( visible : boolean ) => {
167+ if ( ! visible ) {
168+ // Pause non-essential processing
169+ processingPaused = true ;
170+ pauseTime = Date . now ( ) ;
171+ } else {
172+ processingPaused = false ;
173+ pauseTime = null ;
174+ }
175+ } ;
122176
123- handleVisibilityChange ( isVisible ) ;
177+ handleVisibilityChange ( isVisible ) ;
124178
125- if ( ! isVisible ) {
126- // Processing should be paused
127- expect ( processingPaused ) . toBe ( true ) ;
179+ if ( ! isVisible ) {
180+ // Processing should be paused
181+ expect ( processingPaused ) . toBe ( true ) ;
128182
129- // Should pause within 1 second
130- if ( pauseTime !== null ) {
131- const pauseDelay = pauseTime - visibilityChangeTime ;
132- expect ( pauseDelay ) . toBeLessThan ( 1000 ) ;
183+ // Should pause within 1 second
184+ if ( pauseTime !== null ) {
185+ const pauseDelayActual = pauseTime - visibilityChangeTime ;
186+ expect ( pauseDelayActual ) . toBeLessThan ( 1000 ) ;
187+ }
188+ } else {
189+ // Processing should be active
190+ expect ( processingPaused ) . toBe ( false ) ;
133191 }
134- } else {
135- // Processing should be active
136- expect ( processingPaused ) . toBe ( false ) ;
137192 }
138- }
139- ) ,
140- { numRuns : 100 }
141- ) ;
193+ ) ,
194+ { numRuns : 100 }
195+ ) ;
196+ } ) ;
197+
198+ it ( 'VisibilityOptimizer calls pause callbacks when hidden' , ( ) => {
199+ const optimizer = new VisibilityOptimizer ( { pauseDelay : 0 , resumeDelay : 0 } ) ;
200+
201+ let pauseCalled = false ;
202+ let resumeCalled = false ;
203+
204+ optimizer . onPause ( ( ) => { pauseCalled = true ; } ) ;
205+ optimizer . onResume ( ( ) => { resumeCalled = true ; } ) ;
206+
207+ // Initially visible
208+ expect ( optimizer . isVisible ( ) ) . toBe ( true ) ;
209+
210+ optimizer . stop ( ) ;
211+ } ) ;
212+
213+ it ( 'VisibilityOptimizer properly cleans up on stop' , ( ) => {
214+ const optimizer = new VisibilityOptimizer ( ) ;
215+
216+ const unsubscribePause = optimizer . onPause ( ( ) => { } ) ;
217+ const unsubscribeResume = optimizer . onResume ( ( ) => { } ) ;
218+
219+ optimizer . start ( ) ;
220+
221+ // Unsubscribe should work
222+ unsubscribePause ( ) ;
223+ unsubscribeResume ( ) ;
224+
225+ // Stop should not throw
226+ expect ( ( ) => optimizer . stop ( ) ) . not . toThrow ( ) ;
227+ } ) ;
142228 } ) ;
143229
144230 /**
0 commit comments