1+ using System . Diagnostics ;
2+ using System . Globalization ;
3+ using System . IO . Compression ;
4+ using System . Security . Cryptography ;
5+ using System . Text ;
6+ using System . Text . Json ;
7+ using MiniExcelLibs ;
8+ using ManagedMiniExcel = MiniExcelLibs . MiniExcel ;
9+
10+ if ( args . Length == 0 )
11+ return Usage ( ) ;
12+
13+ return args [ 0 ] . ToLowerInvariant ( ) switch
14+ {
15+ "generate" => Generate ( args ) ,
16+ "verify" => Verify ( args ) ,
17+ "managed" => Benchmark ( args , useRust : false ) ,
18+ "rust" => Benchmark ( args , useRust : true ) ,
19+ _ => Usage ( )
20+ } ;
21+
22+ static int Generate ( string [ ] arguments )
23+ {
24+ if ( arguments . Length != 4 ||
25+ ! int . TryParse ( arguments [ 2 ] , out var rowCount ) || rowCount < 1 ||
26+ ! int . TryParse ( arguments [ 3 ] , out var columnCount ) || columnCount is < 1 or > 26 )
27+ return Usage ( ) ;
28+
29+ CreateWorkbook ( Path . GetFullPath ( arguments [ 1 ] ) , rowCount , columnCount ) ;
30+ return 0 ;
31+ }
32+
33+ static int Verify ( string [ ] arguments )
34+ {
35+ if ( arguments . Length != 2 )
36+ return Usage ( ) ;
37+
38+ var path = Path . GetFullPath ( arguments [ 1 ] ) ;
39+ using var managed = Query ( path , useRust : false ) . GetEnumerator ( ) ;
40+ using var rust = Query ( path , useRust : true ) . GetEnumerator ( ) ;
41+ long rowIndex = 0 ;
42+ while ( true )
43+ {
44+ var hasManaged = managed . MoveNext ( ) ;
45+ var hasRust = rust . MoveNext ( ) ;
46+ Require ( hasManaged == hasRust , $ "Row count differs after row { rowIndex } .") ;
47+ if ( ! hasManaged )
48+ break ;
49+ CompareRows ( managed . Current , rust . Current , rowIndex ) ;
50+ rowIndex ++ ;
51+ }
52+
53+ Console . WriteLine ( $ "Verified { rowIndex } rows against MiniExcel 1.46.0.") ;
54+ return 0 ;
55+ }
56+
57+ static int Benchmark ( string [ ] arguments , bool useRust )
58+ {
59+ if ( arguments . Length is < 2 or > 4 ||
60+ arguments . Length >= 3 && ( ! int . TryParse ( arguments [ 2 ] , out var passes ) || passes < 1 ) ||
61+ arguments . Length >= 4 && ( ! int . TryParse ( arguments [ 3 ] , out var warmups ) || warmups < 0 ) )
62+ return Usage ( ) ;
63+
64+ var path = Path . GetFullPath ( arguments [ 1 ] ) ;
65+ var measuredPasses = arguments . Length >= 3 ? int . Parse ( arguments [ 2 ] , CultureInfo . InvariantCulture ) : 1 ;
66+ var warmupPasses = arguments . Length >= 4 ? int . Parse ( arguments [ 3 ] , CultureInfo . InvariantCulture ) : 0 ;
67+ for ( var pass = 0 ; pass < warmupPasses ; pass ++ )
68+ Consume ( path , useRust ) ;
69+
70+ GC . Collect ( ) ;
71+ GC . WaitForPendingFinalizers ( ) ;
72+ GC . Collect ( ) ;
73+ var allocatedBefore = GC . GetTotalAllocatedBytes ( precise : true ) ;
74+ var stopwatch = Stopwatch . StartNew ( ) ;
75+ var firstRowMilliseconds = 0d ;
76+ long rows = 0 ;
77+ long cells = 0 ;
78+ using var hash = IncrementalHash . CreateHash ( HashAlgorithmName . SHA256 ) ;
79+ for ( var pass = 0 ; pass < measuredPasses ; pass ++ )
80+ {
81+ foreach ( var row in Query ( path , useRust ) )
82+ {
83+ if ( rows == 0 )
84+ firstRowMilliseconds = stopwatch . Elapsed . TotalMilliseconds ;
85+ rows ++ ;
86+ cells += row . Count ;
87+ AppendRow ( hash , row ) ;
88+ }
89+ }
90+ stopwatch . Stop ( ) ;
91+
92+ Console . WriteLine ( JsonSerializer . Serialize ( new BenchmarkResult (
93+ useRust ? "MiniExcel.Rust" : "MiniExcel" ,
94+ Environment . Version . ToString ( ) ,
95+ measuredPasses ,
96+ rows ,
97+ cells ,
98+ Convert . ToHexString ( hash . GetHashAndReset ( ) ) ,
99+ stopwatch . Elapsed . TotalMilliseconds ,
100+ firstRowMilliseconds ,
101+ GC . GetTotalAllocatedBytes ( precise : true ) - allocatedBefore ) ) ) ;
102+ return 0 ;
103+ }
104+
105+ static IEnumerable < IDictionary < string , object ? > > Query ( string path , bool useRust )
106+ {
107+ if ( useRust )
108+ return MiniExcelRust . Query ( path , useHeaderRow : false ) ;
109+ return ManagedMiniExcel . Query ( path , useHeaderRow : false )
110+ . Cast < IDictionary < string , object ? > > ( ) ;
111+ }
112+
113+ static void Consume ( string path , bool useRust )
114+ {
115+ foreach ( var row in Query ( path , useRust ) )
116+ _ = row . Count ;
117+ }
118+
119+ static void CompareRows (
120+ IDictionary < string , object ? > managed ,
121+ IDictionary < string , object ? > rust ,
122+ long rowIndex )
123+ {
124+ Require ( managed . Keys . SequenceEqual ( rust . Keys , StringComparer . Ordinal ) ,
125+ $ "Column order differs at row { rowIndex } .") ;
126+ foreach ( var key in managed . Keys )
127+ {
128+ Require ( Normalize ( managed [ key ] ) == Normalize ( rust [ key ] ) ,
129+ $ "Value differs at row { rowIndex } , column { key } : managed={ managed [ key ] } , rust={ rust [ key ] } .") ;
130+ }
131+ }
132+
133+ static void AppendRow ( IncrementalHash hash , IDictionary < string , object ? > row )
134+ {
135+ foreach ( var cell in row )
136+ {
137+ AppendText ( hash , cell . Key ) ;
138+ AppendText ( hash , Normalize ( cell . Value ) ) ;
139+ }
140+ }
141+
142+ static void AppendText ( IncrementalHash hash , string value )
143+ {
144+ var bytes = Encoding . UTF8 . GetBytes ( value ) ;
145+ hash . AppendData ( BitConverter . GetBytes ( bytes . Length ) ) ;
146+ hash . AppendData ( bytes ) ;
147+ }
148+
149+ static string Normalize ( object ? value ) => value switch
150+ {
151+ null or DBNull => "null" ,
152+ IFormattable formattable => formattable . ToString ( null , CultureInfo . InvariantCulture ) ?? string . Empty ,
153+ _ => value . ToString ( ) ?? string . Empty
154+ } ;
155+
156+ static void CreateWorkbook ( string path , int rows , int columns )
157+ {
158+ Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ! ) ;
159+ if ( File . Exists ( path ) )
160+ File . Delete ( path ) ;
161+ using var archive = ZipFile . Open ( path , ZipArchiveMode . Create ) ;
162+ AddEntry ( archive , "[Content_Types].xml" , """
163+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
164+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
165+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
166+ <Default Extension="xml" ContentType="application/xml"/>
167+ <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
168+ <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
169+ </Types>
170+ """ ) ;
171+ AddEntry ( archive , "_rels/.rels" , """
172+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
173+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
174+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
175+ </Relationships>
176+ """ ) ;
177+ AddEntry ( archive , "xl/workbook.xml" , """
178+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
179+ <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
180+ <sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>
181+ </workbook>
182+ """ ) ;
183+ AddEntry ( archive , "xl/_rels/workbook.xml.rels" , """
184+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
185+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
186+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
187+ </Relationships>
188+ """ ) ;
189+
190+ var entry = archive . CreateEntry ( "xl/worksheets/sheet1.xml" , CompressionLevel . Fastest ) ;
191+ using var writer = new StreamWriter ( entry . Open ( ) , new UTF8Encoding ( false ) ) ;
192+ writer . Write ( "<?xml version=\" 1.0\" encoding=\" UTF-8\" standalone=\" yes\" ?><worksheet xmlns=\" http://schemas.openxmlformats.org/spreadsheetml/2006/main\" ><sheetData>" ) ;
193+ for ( var row = 1 ; row <= rows ; row ++ )
194+ {
195+ writer . Write ( $ "<row r=\" { row } \" >") ;
196+ for ( var column = 1 ; column <= columns ; column ++ )
197+ {
198+ var reference = $ "{ ( char ) ( 'A' + column - 1 ) } { row } ";
199+ var value = ( long ) ( row - 1 ) * columns + column ;
200+ writer . Write ( $ "<c r=\" { reference } \" ><v>{ value } </v></c>") ;
201+ }
202+ writer . Write ( "</row>" ) ;
203+ }
204+ writer . Write ( "</sheetData></worksheet>" ) ;
205+ }
206+
207+ static void AddEntry ( ZipArchive archive , string name , string contents )
208+ {
209+ var entry = archive . CreateEntry ( name , CompressionLevel . Fastest ) ;
210+ using var writer = new StreamWriter ( entry . Open ( ) , new UTF8Encoding ( false ) ) ;
211+ writer . Write ( contents ) ;
212+ }
213+
214+ static void Require ( bool condition , string message )
215+ {
216+ if ( ! condition )
217+ throw new InvalidOperationException ( message ) ;
218+ }
219+
220+ static int Usage ( )
221+ {
222+ Console . Error . WriteLine ( "Usage:" ) ;
223+ Console . Error . WriteLine ( " NuGetV1Query generate <xlsx-path> <rows> <columns>" ) ;
224+ Console . Error . WriteLine ( " NuGetV1Query verify <xlsx-path>" ) ;
225+ Console . Error . WriteLine ( " NuGetV1Query <managed|rust> <xlsx-path> [passes] [warmup-passes]" ) ;
226+ return 2 ;
227+ }
228+
229+ internal sealed record BenchmarkResult (
230+ string Runtime ,
231+ string DotNetRuntime ,
232+ int Passes ,
233+ long Rows ,
234+ long Cells ,
235+ string ContentHash ,
236+ double ElapsedMilliseconds ,
237+ double FirstRowMilliseconds ,
238+ long AllocatedBytes ) ;
0 commit comments