@@ -425,5 +425,225 @@ public async Task ExportPicture_DuplicateUrls_AllExported()
425425 }
426426
427427 #endregion
428+
429+ #region 图片导出性能基准测试
430+
431+ /// <summary>
432+ /// 测试使用的图片资源(绝对路径)。
433+ /// 覆盖多种格式、不同尺寸、不同文件大小,用于模拟真实场景。
434+ /// </summary>
435+ private static readonly string [ ] TestImagePaths = new [ ]
436+ {
437+ Path . Combine ( "TestFiles" , "ExporterTest.png" ) ,
438+ Path . Combine ( "TestFiles" , "Images" , "1.Jpeg" ) ,
439+ Path . Combine ( "TestFiles" , "Images" , "2.Jpeg" ) ,
440+ Path . Combine ( "TestFiles" , "Images" , "3.Jpeg" ) ,
441+ Path . Combine ( "TestFiles" , "Images" , "4.Jpeg" ) ,
442+ Path . Combine ( "TestFiles" , "Images" , "zero-DPI.Jpeg" ) ,
443+ } ;
444+
445+ /// <summary>
446+ /// 为每行数据分配图片:奇数行 Img1 / Img 使用不同图片,偶数行使用同一图片,
447+ /// 用于同时测试去重缓存命中与多源图片加载。
448+ /// </summary>
449+ private static void SeedImages ( System . Collections . Generic . IList < ExportTestDataWithPicture > data , string [ ] imagePool )
450+ {
451+ for ( int i = 0 ; i < data . Count ; i ++ )
452+ {
453+ var img1 = imagePool [ i % imagePool . Length ] ;
454+ var img = imagePool [ ( i / 2 ) % imagePool . Length ] ;
455+ data [ i ] . Img1 = img1 ;
456+ data [ i ] . Img = img ;
457+ }
458+ }
459+
460+ [ Fact ( DisplayName = "图片导出性能-500行双图片导出耗时测量" ) ]
461+ public async Task ExportPicture_Performance_500Rows_DoubleImages ( )
462+ {
463+ IExporter exporter = new ExcelExporter ( ) ;
464+ var filePath = GetTestFilePath ( $ "{ nameof ( ExportPicture_Performance_500Rows_DoubleImages ) } .xlsx") ;
465+ DeleteFile ( filePath ) ;
466+
467+ var data = GenFu . GenFu . ListOf < ExportTestDataWithPicture > ( 500 ) ;
468+ var imagePath = Path . Combine ( "TestFiles" , "ExporterTest.png" ) ;
469+ foreach ( var item in data )
470+ {
471+ item . Img1 = imagePath ;
472+ item . Img = imagePath ;
473+ }
474+
475+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
476+ var result = await exporter . Export ( filePath , data ) ;
477+ sw . Stop ( ) ;
478+
479+ result . ShouldNotBeNull ( ) ;
480+ new FileInfo ( filePath ) . Exists . ShouldBeTrue ( ) ;
481+
482+ var fileSizeKB = new FileInfo ( filePath ) . Length / 1024.0 ;
483+
484+ using ( var pck = new ExcelPackage ( new FileInfo ( filePath ) ) )
485+ {
486+ var sheet = pck . Workbook . Worksheets . First ( ) ;
487+ // 500 行 × 2 列 = 1000 个 drawing
488+ sheet . Drawings . Count . ShouldBe ( 1000 ) ;
489+ sheet . Dimension . Rows . ShouldBe ( 501 ) ; // 1 header + 500 data
490+
491+ _output . WriteLine ( $ "✅ 导出完成") ;
492+ _output . WriteLine ( $ " 行数: 500 × 2 列图片 = 1000 个 Drawing") ;
493+ _output . WriteLine ( $ " 耗时: { sw . ElapsedMilliseconds } ms ({ sw . Elapsed . TotalSeconds : F1} s)") ;
494+ _output . WriteLine ( $ " 文件大小: { fileSizeKB : F1} KB") ;
495+ _output . WriteLine ( $ " 平均每行: { sw . ElapsedMilliseconds / 500.0 : F1} ms/行") ;
496+ _output . WriteLine ( $ " 图片去重: 500 行共用同一图片文件(测试去重逻辑)") ;
497+ }
498+ }
499+
500+ [ Fact ( DisplayName = "图片导出性能-2000行双图片导出耗时测量" ) ]
501+ public async Task ExportPicture_Performance_2000Rows_DoubleImages ( )
502+ {
503+ IExporter exporter = new ExcelExporter ( ) ;
504+ var filePath = GetTestFilePath ( $ "{ nameof ( ExportPicture_Performance_2000Rows_DoubleImages ) } .xlsx") ;
505+ DeleteFile ( filePath ) ;
506+
507+ var data = GenFu . GenFu . ListOf < ExportTestDataWithPicture > ( 2000 ) ;
508+ var imagePath = Path . Combine ( "TestFiles" , "ExporterTest.png" ) ;
509+ foreach ( var item in data )
510+ {
511+ item . Img1 = imagePath ;
512+ item . Img = imagePath ;
513+ }
514+
515+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
516+ var result = await exporter . Export ( filePath , data ) ;
517+ sw . Stop ( ) ;
518+
519+ result . ShouldNotBeNull ( ) ;
520+
521+ var fileSizeKB = new FileInfo ( filePath ) . Length / 1024.0 ;
522+
523+ using ( var pck = new ExcelPackage ( new FileInfo ( filePath ) ) )
524+ {
525+ var sheet = pck . Workbook . Worksheets . First ( ) ;
526+ sheet . Drawings . Count . ShouldBe ( 4000 ) ;
527+ sheet . Dimension . Rows . ShouldBe ( 2001 ) ; // 1 header + 2000 data
528+
529+ _output . WriteLine ( $ "✅ 导出完成") ;
530+ _output . WriteLine ( $ " 行数: 2000 × 2 列图片 = 4000 个 Drawing") ;
531+ _output . WriteLine ( $ " 耗时: { sw . ElapsedMilliseconds } ms ({ sw . Elapsed . TotalSeconds : F1} s)") ;
532+ _output . WriteLine ( $ " 文件大小: { fileSizeKB : F1} KB") ;
533+ _output . WriteLine ( $ " 平均每行: { sw . ElapsedMilliseconds / 2000.0 : F1} ms/行") ;
534+ _output . WriteLine ( $ " 图片去重: 2000 行共用同一图片文件(测试去重逻辑)") ;
535+ }
536+ }
537+
538+ [ Fact ( DisplayName = "图片导出压力测试-1000行双图片" ) ]
539+ public async Task ExportPicture_Stress_1000Rows_DoubleImages ( )
540+ {
541+ IExporter exporter = new ExcelExporter ( ) ;
542+ var filePath = GetTestFilePath ( $ "{ nameof ( ExportPicture_Stress_1000Rows_DoubleImages ) } .xlsx") ;
543+ DeleteFile ( filePath ) ;
544+
545+ var data = GenFu . GenFu . ListOf < ExportTestDataWithPicture > ( 1000 ) ;
546+ var imagePath = Path . Combine ( "TestFiles" , "ExporterTest.png" ) ;
547+ foreach ( var item in data )
548+ {
549+ item . Img1 = imagePath ;
550+ item . Img = imagePath ;
551+ }
552+
553+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
554+ var result = await exporter . Export ( filePath , data ) ;
555+ sw . Stop ( ) ;
556+
557+ result . ShouldNotBeNull ( ) ;
558+
559+ var fileSizeKB = new FileInfo ( filePath ) . Length / 1024.0 ;
560+
561+ using ( var pck = new ExcelPackage ( new FileInfo ( filePath ) ) )
562+ {
563+ var sheet = pck . Workbook . Worksheets . First ( ) ;
564+ sheet . Drawings . Count . ShouldBe ( 2000 ) ;
565+ sheet . Dimension . Rows . ShouldBe ( 1001 ) ; // 1 header + 1000 data
566+
567+ _output . WriteLine ( $ "✅ 1000 行压力测试完成") ;
568+ _output . WriteLine ( $ " 行数: 1000 × 2 列图片 = 2000 个 Drawing") ;
569+ _output . WriteLine ( $ " 耗时: { sw . ElapsedMilliseconds } ms ({ sw . Elapsed . TotalSeconds : F1} s)") ;
570+ _output . WriteLine ( $ " 文件大小: { fileSizeKB : F1} KB") ;
571+ _output . WriteLine ( $ " 平均每行: { sw . ElapsedMilliseconds / 1000.0 : F2} ms/行") ;
572+ }
573+ }
574+
575+ [ Fact ( DisplayName = "图片导出压力测试-1000行多图混排(PNG/JPEG混排 + 部分无效URL)" ) ]
576+ public async Task ExportPicture_Stress_1000Rows_MultiImageMix ( )
577+ {
578+ IExporter exporter = new ExcelExporter ( ) ;
579+ var filePath = GetTestFilePath ( $ "{ nameof ( ExportPicture_Stress_1000Rows_MultiImageMix ) } .xlsx") ;
580+ DeleteFile ( filePath ) ;
581+
582+ // 1000 行数据,列 Img1/Img 使用 6 张图片轮询,模拟多图混排
583+ var data = GenFu . GenFu . ListOf < ExportTestDataWithPicture > ( 1000 ) ;
584+ SeedImages ( data , TestImagePaths ) ;
585+
586+ // 在末尾追加 50 行混合有效/无效/Base64/空值的情况,覆盖所有图片加载分支
587+ var boundary = GenFu . GenFu . ListOf < ExportTestDataWithPicture > ( 50 ) ;
588+ var pngBytes = File . ReadAllBytes ( Path . Combine ( "TestFiles" , "ExporterTest.png" ) ) ;
589+ var pngBase64 = Convert . ToBase64String ( pngBytes ) ;
590+ for ( int i = 0 ; i < boundary . Count ; i ++ )
591+ {
592+ switch ( i % 5 )
593+ {
594+ case 0 : // 有效本地图片
595+ boundary [ i ] . Img1 = TestImagePaths [ i % TestImagePaths . Length ] ;
596+ boundary [ i ] . Img = TestImagePaths [ ( i + 1 ) % TestImagePaths . Length ] ;
597+ break ;
598+ case 1 : // 无效 URL(Alt 文本兜底)
599+ boundary [ i ] . Img1 = $ "missing-{ i } .jpg";
600+ boundary [ i ] . Img = TestImagePaths [ i % TestImagePaths . Length ] ;
601+ break ;
602+ case 2 : // Base64
603+ boundary [ i ] . Img1 = pngBase64 ;
604+ boundary [ i ] . Img = null ;
605+ break ;
606+ case 3 : // null(占位行)
607+ boundary [ i ] . Img1 = null ;
608+ boundary [ i ] . Img = null ;
609+ break ;
610+ case 4 : // 只填 Img
611+ boundary [ i ] . Img1 = null ;
612+ boundary [ i ] . Img = TestImagePaths [ i % TestImagePaths . Length ] ;
613+ break ;
614+ }
615+ }
616+ foreach ( var item in boundary ) data . Add ( item ) ;
617+
618+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
619+ var result = await exporter . Export ( filePath , data ) ;
620+ sw . Stop ( ) ;
621+
622+ result . ShouldNotBeNull ( ) ;
623+ new FileInfo ( filePath ) . Exists . ShouldBeTrue ( ) ;
624+
625+ var fileSizeKB = new FileInfo ( filePath ) . Length / 1024.0 ;
626+
627+ using ( var pck = new ExcelPackage ( new FileInfo ( filePath ) ) )
628+ {
629+ var sheet = pck . Workbook . Worksheets . First ( ) ;
630+
631+ // 1000 行每行 2 列 + 边界 50 行每行 1~2 列 = 2000 + 60 = 2060 个 drawing
632+ // 边界中 60% 行有 Img1、80% 行有 Img
633+ var expectedMin = 1000 * 2 + 50 ; // 最坏情况:边界行每行至少 1 张图
634+ sheet . Drawings . Count . ShouldBeGreaterThanOrEqualTo ( expectedMin ) ;
635+ sheet . Dimension . Rows . ShouldBe ( 1051 ) ; // 1 header + 1050 data
636+
637+ _output . WriteLine ( $ "✅ 多图混排压力测试完成") ;
638+ _output . WriteLine ( $ " 数据行: 1050(1000 多图轮询 + 50 边界混合)") ;
639+ _output . WriteLine ( $ " Drawing 数量: { sheet . Drawings . Count } ") ;
640+ _output . WriteLine ( $ " 耗时: { sw . ElapsedMilliseconds } ms ({ sw . Elapsed . TotalSeconds : F1} s)") ;
641+ _output . WriteLine ( $ " 文件大小: { fileSizeKB : F1} KB") ;
642+ _output . WriteLine ( $ " 平均每行: { sw . ElapsedMilliseconds / 1050.0 : F2} ms/行") ;
643+ _output . WriteLine ( $ " 图片源: { TestImagePaths . Length } 个本地图片循环 + Base64 + 失效 URL 兜底") ;
644+ }
645+ }
646+
647+ #endregion
428648 }
429649}
0 commit comments