Skip to content

Commit d47fbc7

Browse files
authored
Optimize PipeTable parsing: O(n²) → O(n) for 3.7x–85x speedup, enables 10K+ row tables (xoofx#922)
* Optimize PipeTable parsing: O(n²) → O(n) for large tables Pipe tables were creating deeply nested tree structures where each pipe delimiter contained all subsequent content as children, causing O(n²) traversal complexity for n cells. This change restructures the parser to use a flat sibling-based structure, treating tables as matrices rather than nested trees. Key changes: - Set IsClosed=true on PipeTableDelimiterInline to prevent nesting - Add PromoteNestedPipesToRootLevel() to flatten pipes nested in emphasis - Update cell boundary detection to use sibling traversal - Move EmphasisInlineParser before PipeTableParser in processing order - Fix EmphasisInlineParser to continue past IsClosed delimiters - Add ContainsParentOrSiblingOfType<T>() helper for flat structure detection Performance improvements (measured on typical markdown content): | Rows | Before | After | Speedup | |------|-----------|---------|---------| | 100 | 542 μs | 150 μs | 3.6x | | 500 | 23,018 μs | 763 μs | 30x | | 1000 | 89,418 μs | 1,596 μs| 56x | | 1500 | 201,593 μs| 2,740 μs| 74x | | 5000 | CRASH | 10,588 μs| ∞ | | 10000| CRASH | 18,551 μs| ∞ | Tables with 5000+ rows previously crashed due to stack overflow from recursive depth. They now parse successfully with linear time complexity. * remove baseline results file * Do not use System.Index and fix nullabillity checks for older platforms
1 parent 3602433 commit d47fbc7

10 files changed

Lines changed: 424 additions & 138 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*.sln.docstates
99
*.nuget.props
1010
*.nuget.targets
11+
src/.idea
12+
BenchmarkDotNet.Artifacts
1113

1214
# User-specific files (MonoDevelop/Xamarin Studio)
1315
*.userprefs
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// This file is licensed under the BSD-Clause 2 license.
3+
// See the license.txt file in the project root for more information.
4+
5+
using BenchmarkDotNet.Attributes;
6+
using BenchmarkDotNet.Diagnosers;
7+
using Markdig;
8+
9+
namespace Testamina.Markdig.Benchmarks.PipeTable;
10+
11+
/// <summary>
12+
/// Benchmark for pipe table parsing performance, especially for large tables.
13+
/// Tests the performance of PipeTableParser with varying table sizes.
14+
/// </summary>
15+
[MemoryDiagnoser]
16+
[GcServer(true)] // Use server GC to get more comprehensive GC stats
17+
public class PipeTableBenchmark
18+
{
19+
private string _100Rows = null!;
20+
private string _500Rows = null!;
21+
private string _1000Rows = null!;
22+
private string _1500Rows = null!;
23+
private string _5000Rows = null!;
24+
private string _10000Rows = null!;
25+
private MarkdownPipeline _pipeline = null!;
26+
27+
[GlobalSetup]
28+
public void Setup()
29+
{
30+
// Pipeline with pipe tables enabled (part of advanced extensions)
31+
_pipeline = new MarkdownPipelineBuilder()
32+
.UseAdvancedExtensions()
33+
.Build();
34+
35+
// Generate tables of various sizes
36+
// Note: Before optimization, 5000+ rows hit depth limit due to nested tree structure.
37+
// After optimization, these should work.
38+
_100Rows = PipeTableGenerator.Generate(rows: 100, columns: 5);
39+
_500Rows = PipeTableGenerator.Generate(rows: 500, columns: 5);
40+
_1000Rows = PipeTableGenerator.Generate(rows: 1000, columns: 5);
41+
_1500Rows = PipeTableGenerator.Generate(rows: 1500, columns: 5);
42+
_5000Rows = PipeTableGenerator.Generate(rows: 5000, columns: 5);
43+
_10000Rows = PipeTableGenerator.Generate(rows: 10000, columns: 5);
44+
}
45+
46+
[Benchmark(Description = "PipeTable 100 rows x 5 cols")]
47+
public string Parse100Rows()
48+
{
49+
return Markdown.ToHtml(_100Rows, _pipeline);
50+
}
51+
52+
[Benchmark(Description = "PipeTable 500 rows x 5 cols")]
53+
public string Parse500Rows()
54+
{
55+
return Markdown.ToHtml(_500Rows, _pipeline);
56+
}
57+
58+
[Benchmark(Description = "PipeTable 1000 rows x 5 cols")]
59+
public string Parse1000Rows()
60+
{
61+
return Markdown.ToHtml(_1000Rows, _pipeline);
62+
}
63+
64+
[Benchmark(Description = "PipeTable 1500 rows x 5 cols")]
65+
public string Parse1500Rows()
66+
{
67+
return Markdown.ToHtml(_1500Rows, _pipeline);
68+
}
69+
70+
[Benchmark(Description = "PipeTable 5000 rows x 5 cols")]
71+
public string Parse5000Rows()
72+
{
73+
return Markdown.ToHtml(_5000Rows, _pipeline);
74+
}
75+
76+
[Benchmark(Description = "PipeTable 10000 rows x 5 cols")]
77+
public string Parse10000Rows()
78+
{
79+
return Markdown.ToHtml(_10000Rows, _pipeline);
80+
}
81+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// This file is licensed under the BSD-Clause 2 license.
3+
// See the license.txt file in the project root for more information.
4+
5+
using System.Text;
6+
7+
namespace Testamina.Markdig.Benchmarks.PipeTable;
8+
9+
/// <summary>
10+
/// Generates pipe table markdown content for benchmarking purposes.
11+
/// </summary>
12+
public static class PipeTableGenerator
13+
{
14+
private const int DefaultCellWidth = 10;
15+
16+
/// <summary>
17+
/// Generates a pipe table in markdown format.
18+
/// </summary>
19+
/// <param name="rows">Number of data rows (excluding header)</param>
20+
/// <param name="columns">Number of columns</param>
21+
/// <param name="cellWidth">Width of each cell content (default: 10)</param>
22+
/// <returns>Pipe table markdown string</returns>
23+
public static string Generate(int rows, int columns, int cellWidth = DefaultCellWidth)
24+
{
25+
var sb = new StringBuilder();
26+
27+
// Header row
28+
sb.Append('|');
29+
for (int col = 0; col < columns; col++)
30+
{
31+
sb.Append(' ');
32+
sb.Append($"Header {col + 1}".PadRight(cellWidth));
33+
sb.Append(" |");
34+
}
35+
sb.AppendLine();
36+
37+
// Separator row (with dashes)
38+
sb.Append('|');
39+
for (int col = 0; col < columns; col++)
40+
{
41+
sb.Append(new string('-', cellWidth + 2));
42+
sb.Append('|');
43+
}
44+
sb.AppendLine();
45+
46+
// Data rows
47+
for (int row = 0; row < rows; row++)
48+
{
49+
sb.Append('|');
50+
for (int col = 0; col < columns; col++)
51+
{
52+
sb.Append(' ');
53+
sb.Append($"R{row + 1}C{col + 1}".PadRight(cellWidth));
54+
sb.Append(" |");
55+
}
56+
sb.AppendLine();
57+
}
58+
59+
return sb.ToString();
60+
}
61+
}

src/Markdig.Benchmarks/Program.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using BenchmarkDotNet.Running;
88

99
using Markdig;
10+
using Testamina.Markdig.Benchmarks.PipeTable;
1011

1112

1213
namespace Testamina.Markdig.Benchmarks;
@@ -68,7 +69,16 @@ static void Main(string[] args)
6869
//config.Add(gcDiagnoser);
6970

7071
//var config = DefaultConfig.Instance;
71-
BenchmarkRunner.Run<Program>(config);
72+
73+
// Run specific benchmarks based on command line arguments
74+
if (args.Length > 0 && args[0] == "--pipetable")
75+
{
76+
BenchmarkRunner.Run<PipeTableBenchmark>(config);
77+
}
78+
else
79+
{
80+
BenchmarkRunner.Run<Program>(config);
81+
}
7282
//BenchmarkRunner.Run<TestDictionary>(config);
7383
//BenchmarkRunner.Run<TestMatchPerf>();
7484
//BenchmarkRunner.Run<TestStringPerf>();

src/Markdig/Extensions/Tables/PipeTableExtension.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void Setup(MarkdownPipelineBuilder pipeline)
3838
var lineBreakParser = pipeline.InlineParsers.FindExact<LineBreakInlineParser>();
3939
if (!pipeline.InlineParsers.Contains<PipeTableParser>())
4040
{
41-
pipeline.InlineParsers.InsertBefore<EmphasisInlineParser>(new PipeTableParser(lineBreakParser!, Options));
41+
pipeline.InlineParsers.InsertAfter<EmphasisInlineParser>(new PipeTableParser(lineBreakParser!, Options));
4242
}
4343
}
4444

0 commit comments

Comments
 (0)