@@ -139,3 +139,147 @@ assert.throws(() => importBytes(map([
139139 assert . throws ( ( ) => importBytes ( map ( [ ...kLayout , counts ( 5 , 2 , 0 , 1 ) ] ) ) ,
140140 kInvalid ) ;
141141}
142+
143+ // --- Format versions ---
144+
145+ function assertSameHistogram ( actual , expected ) {
146+ assert . strictEqual ( actual . count , expected . count ) ;
147+ assert . strictEqual ( actual . min , expected . min ) ;
148+ assert . strictEqual ( actual . max , expected . max ) ;
149+ assert . strictEqual ( actual . percentile ( 50 ) , expected . percentile ( 50 ) ) ;
150+ assert . strictEqual ( actual . percentile ( 100 ) , expected . percentile ( 100 ) ) ;
151+ }
152+
153+ {
154+ // Version 1 data, as produced by Node.js v26.9.0, remains importable.
155+ const v1 = Buffer . from (
156+ 'ac00010101021b001fffffffffffff030304070501061a075bcd15070008fb3f' +
157+ 'f00000000000000919b0000a8c010101010101190bfd02191fa101191bba010b' +
158+ 'a500fb3fc45d819a94b14c01fb4172dc620791dc2002fb431ce79e5650942003' +
159+ 'fb3fd2bec33301886804191388' , 'hex' ) ;
160+ const h = importHistogram ( new Uint8Array ( v1 ) ) ;
161+ assert . strictEqual ( h . count , 7 ) ;
162+ assert . strictEqual ( h . min , 1 ) ;
163+ assert . strictEqual ( h . max , 123469823 ) ;
164+ assert . strictEqual ( h . mean , 17777921.42857143 ) ;
165+ assert . strictEqual ( h . stddev , 43136537.01467338 ) ;
166+ assert . strictEqual ( h . percentile ( 50 ) , 4099 ) ;
167+ assert . strictEqual ( h . percentile ( 100 ) , 123469823 ) ;
168+ assert . strictEqual ( h . ewmaMean , 19777056.47311032 ) ;
169+ assert . strictEqual ( h . ewmaStddev , 45099796.52633914 ) ;
170+ assert . strictEqual ( h . ewmaErrorRate , 0.29289321881345254 ) ;
171+ }
172+
173+ {
174+ // export() produces version 2 data, which round-trips.
175+ const h = createHistogram ( { halfLife : 4 , threshold : 5000 } ) ;
176+ for ( const value of [ 1 , 2 , 3 , 4096 , 4097 , 1000000 , 123456789 ] ) {
177+ h . record ( value ) ;
178+ }
179+ const data = h . export ( ) ;
180+ // The first map entry is the version.
181+ assert . deepStrictEqual ( [ ...data . subarray ( 1 , 3 ) ] , [ ...uint ( 0 ) , ...uint ( 2 ) ] ) ;
182+ const h2 = importHistogram ( data ) ;
183+ assertSameHistogram ( h2 , h ) ;
184+ assert . strictEqual ( h2 . ewmaMean , h . ewmaMean ) ;
185+ assert . strictEqual ( h2 . ewmaErrorRate , h . ewmaErrorRate ) ;
186+ }
187+
188+ {
189+ // Unknown keys in version 2 data are ignored, whatever their values are.
190+ const unknownValues = [
191+ uint ( 2n ** 40n ) ,
192+ negint ( 7 ) ,
193+ [ ...head ( 2 , 3 ) , 1 , 2 , 3 ] , // Byte string.
194+ text ( 'hello' ) ,
195+ array ( [ uint ( 1 ) , array ( [ text ( 'nested' ) ] ) ] ) ,
196+ map ( [ [ text ( 'a' ) , uint ( 1 ) ] , [ uint ( 99 ) , map ( [ [ uint ( 1 ) , f64 ( 2.5 ) ] ] ) ] ] ) ,
197+ [ ...head ( 6 , 1 ) , ...text ( '2026-09-17' ) ] , // Tag.
198+ f64 ( 1.5 ) ,
199+ [ 0xf9 , 0x3e , 0x00 ] , // Float16.
200+ [ 0xfa , 0x3f , 0xc0 , 0x00 , 0x00 ] , // Float32.
201+ [ 0xf5 ] , // true
202+ [ 0xf6 ] , // null
203+ [ 0xf8 , 0xff ] , // Simple value 255.
204+ ] ;
205+ const expected = importBytes (
206+ map ( [ [ uint ( 0 ) , uint ( 2 ) ] , ...kLayout , counts ( 5 , 2 , 3 , 1 ) ] ) ) ;
207+ const h = importBytes ( map ( [
208+ [ uint ( 0 ) , uint ( 2 ) ] ,
209+ ...kLayout ,
210+ ...unknownValues . map ( ( value , n ) => [ uint ( 12 + n ) , value ] ) ,
211+ counts ( 5 , 2 , 3 , 1 ) ,
212+ ] ) ) ;
213+ assertSameHistogram ( h , expected ) ;
214+
215+ // Unknown keys may appear before the version key.
216+ assertSameHistogram ( importBytes ( map ( [
217+ [ uint ( 12 ) , text ( 'before the version' ) ] ,
218+ [ uint ( 0 ) , uint ( 2 ) ] ,
219+ ...kLayout ,
220+ counts ( 5 , 2 , 3 , 1 ) ,
221+ ] ) ) , expected ) ;
222+
223+ // Unknown keys in the EWMA state are ignored as well.
224+ const withEwma = importBytes ( map ( [
225+ [ uint ( 0 ) , uint ( 2 ) ] ,
226+ ...kLayout ,
227+ counts ( 5 , 2 , 3 , 1 ) ,
228+ [ uint ( 11 ) , map ( [
229+ [ uint ( 0 ) , f64 ( 0.5 ) ] ,
230+ [ uint ( 99 ) , text ( 'unknown' ) ] ,
231+ [ uint ( 1 ) , f64 ( 6 ) ] ,
232+ ] ) ] ,
233+ ] ) ) ;
234+ assertSameHistogram ( withEwma , expected ) ;
235+ assert . strictEqual ( withEwma . ewmaMean , 6 ) ;
236+ }
237+
238+ {
239+ // Version 1 data, or data without a version, keeps the original semantics:
240+ // unknown keys are rejected.
241+ const unknown = [ uint ( 12 ) , uint ( 0 ) ] ;
242+ assert . throws ( ( ) => importBytes ( map ( [ [ uint ( 0 ) , uint ( 1 ) ] , ...kLayout , unknown ] ) ) ,
243+ kInvalid ) ;
244+ assert . throws ( ( ) => importBytes ( map ( [ unknown , [ uint ( 0 ) , uint ( 1 ) ] , ...kLayout ] ) ) ,
245+ kInvalid ) ;
246+ assert . throws ( ( ) => importBytes ( map ( [ ...kLayout , unknown ] ) ) , kInvalid ) ;
247+ assert . throws ( ( ) => importBytes ( map ( [
248+ [ uint ( 0 ) , uint ( 1 ) ] ,
249+ ...kLayout ,
250+ [ uint ( 11 ) , map ( [ [ uint ( 99 ) , uint ( 0 ) ] ] ) ] ,
251+ ] ) ) , kInvalid ) ;
252+ }
253+
254+ // Other versions are rejected.
255+ for ( const version of [ 0 , 3 , 99 ] ) {
256+ assert . throws (
257+ ( ) => importBytes ( map ( [ [ uint ( 0 ) , uint ( version ) ] , ...kLayout ] ) ) ,
258+ kInvalid ) ;
259+ }
260+
261+ {
262+ // Values of unknown keys must still be well-formed.
263+ const withUnknownValue =
264+ ( value ) => map ( [ [ uint ( 0 ) , uint ( 2 ) ] , ...kLayout , [ uint ( 12 ) , value ] ] ) ;
265+
266+ // Indefinite-length items are not supported.
267+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ 0x5f , 0x41 , 0x00 , 0xff ] ) ) ,
268+ kInvalid ) ;
269+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ 0x9f , 0x00 , 0xff ] ) ) ,
270+ kInvalid ) ;
271+ // Reserved additional information and break codes.
272+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ 0x1c ] ) ) , kInvalid ) ;
273+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ 0xfc ] ) ) , kInvalid ) ;
274+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ 0xff ] ) ) , kInvalid ) ;
275+ // Truncated values.
276+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ ...head ( 3 , 10 ) , 0x61 ] ) ) ,
277+ kInvalid ) ;
278+ assert . throws ( ( ) => importBytes ( withUnknownValue ( [ 0xfb , 0x00 , 0x00 ] ) ) ,
279+ kInvalid ) ;
280+ assert . throws ( ( ) => importBytes ( withUnknownValue ( head ( 4 , 5 ) ) ) , kInvalid ) ;
281+ // Nesting is limited to 16 levels.
282+ const nested = ( depth ) => [ ...new Array ( depth ) . fill ( 0x81 ) , 0x00 ] ;
283+ assert . strictEqual ( importBytes ( withUnknownValue ( nested ( 16 ) ) ) . count , 0 ) ;
284+ assert . throws ( ( ) => importBytes ( withUnknownValue ( nested ( 17 ) ) ) , kInvalid ) ;
285+ }
0 commit comments