@@ -6,12 +6,36 @@ module CI
66 module Queue
77 module Strategy
88 class SuiteBinPacking < Base
9- def order_tests ( tests , random : Random . new , config : nil )
10- timing_data = load_timing_data ( config &.timing_file )
11- max_duration = config &.suite_max_duration || 120_000
12- fallback_duration = config &.timing_fallback_duration || 100.0
13- buffer_percent = config &.suite_buffer_percent || 10
9+ class << self
10+ def load_timing_data ( file_path )
11+ return { } unless file_path && ::File . exist? ( file_path )
12+
13+ JSON . parse ( ::File . read ( file_path ) )
14+ rescue JSON ::ParserError => e
15+ warn "Warning: Could not parse timing file #{ file_path } : #{ e . message } "
16+ { }
17+ end
18+ end
19+
20+ def initialize ( config , redis : nil )
21+ super ( config )
22+
23+ if redis
24+ @moving_average = CI ::Queue ::Redis ::MovingAverage . new ( redis )
25+ end
26+
27+ if config &.timing_file
28+ @timing_data = self . class . load_timing_data ( config . timing_file )
29+ else
30+ @timing_data = { }
31+ end
1432
33+ @max_duration = config &.suite_max_duration || 120_000
34+ @fallback_duration = config &.timing_fallback_duration || 100.0
35+ @buffer_percent = config &.suite_buffer_percent || 10
36+ end
37+
38+ def order_tests ( tests , random : ::Random . new , redis : nil )
1539 # Group tests by suite name
1640 suites = tests . group_by { |test | extract_suite_name ( test . id ) }
1741
@@ -22,10 +46,6 @@ def order_tests(tests, random: Random.new, config: nil)
2246 create_chunks_for_suite (
2347 suite_name ,
2448 suite_tests ,
25- max_duration ,
26- buffer_percent ,
27- timing_data ,
28- fallback_duration
2949 )
3050 )
3151 end
@@ -40,27 +60,27 @@ def extract_suite_name(test_id)
4060 test_id . split ( '#' ) . first
4161 end
4262
43- def load_timing_data ( file_path )
44- return { } unless file_path && ::File . exist? ( file_path )
45-
46- JSON . parse ( ::File . read ( file_path ) )
47- rescue JSON ::ParserError => e
48- warn "Warning: Could not parse timing file #{ file_path } : #{ e . message } "
49- { }
50- end
63+ def get_test_duration ( test_id )
64+ if @moving_average
65+ avg = @moving_average [ test_id ]
66+ return avg if avg
67+ end
5168
52- def get_test_duration ( test_id , timing_data , fallback_duration )
53- timing_data [ test_id ] &.to_f || fallback_duration
69+ if @timing_data . key? ( test_id )
70+ @timing_data [ test_id ]
71+ else
72+ @fallback_duration
73+ end
5474 end
5575
56- def create_chunks_for_suite ( suite_name , suite_tests , max_duration , buffer_percent , timing_data , fallback_duration )
76+ def create_chunks_for_suite ( suite_name , suite_tests )
5777 # Calculate total suite duration
5878 total_duration = suite_tests . sum do |test |
59- get_test_duration ( test . id , timing_data , fallback_duration )
79+ get_test_duration ( test . id )
6080 end
6181
6282 # If suite fits in max duration, create full_suite chunk
63- if total_duration <= max_duration
83+ if total_duration <= @ max_duration
6484 chunk_id = "#{ suite_name } :full_suite"
6585 # Don't store test_ids in Redis - worker will resolve from index
6686 # But pass test_count for timeout calculation
@@ -71,20 +91,16 @@ def create_chunks_for_suite(suite_name, suite_tests, max_duration, buffer_percen
7191 split_suite_into_chunks (
7292 suite_name ,
7393 suite_tests ,
74- max_duration ,
75- buffer_percent ,
76- timing_data ,
77- fallback_duration
7894 )
7995 end
8096
81- def split_suite_into_chunks ( suite_name , suite_tests , max_duration , buffer_percent , timing_data , fallback_duration )
97+ def split_suite_into_chunks ( suite_name , suite_tests )
8298 # Apply buffer to max duration
83- effective_max = max_duration * ( 1 - buffer_percent / 100.0 )
99+ effective_max = @ max_duration * ( 1 - @ buffer_percent / 100.0 )
84100
85101 # Sort tests by duration (longest first for better bin packing)
86102 sorted_tests = suite_tests . sort_by do |test |
87- -get_test_duration ( test . id , timing_data , fallback_duration )
103+ -get_test_duration ( test . id )
88104 end
89105
90106 # First-fit decreasing bin packing
@@ -94,7 +110,7 @@ def split_suite_into_chunks(suite_name, suite_tests, max_duration, buffer_percen
94110 chunk_index = 0
95111
96112 sorted_tests . each do |test |
97- test_duration = get_test_duration ( test . id , timing_data , fallback_duration )
113+ test_duration = get_test_duration ( test . id )
98114
99115 if current_chunk_duration + test_duration > effective_max && current_chunk_tests . any?
100116 # Finalize current chunk and start new one
0 commit comments