Skip to content

Commit 5c231f6

Browse files
committed
Penalty and mask statistics
1 parent d00fdfc commit 5c231f6

3 files changed

Lines changed: 261 additions & 19 deletions

File tree

QrCodeGeneratorProfiling/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace Net.Codecrete.QrCodeGenerator.Profiling;
2222
/// <item><c>benchmark</c> — runs BenchmarkDotNet for statistically sound measurements.</item>
2323
/// <item><c>profile [iterations]</c> — runs a plain loop suitable for attaching
2424
/// JetBrains Rider's dotTrace or dotMemory to identify hotspots in <see cref="QrCode.EncodeText"/>.</item>
25+
/// <item><c>stats</c> — collects penalty-contribution and mask-pattern-selection statistics
26+
/// over the sample data and prints them as Markdown tables.</item>
2527
/// </list>
2628
/// </remarks>
2729
public static class Program
@@ -48,6 +50,10 @@ public static int Main(string[] args)
4850
RunProfileLoop(iterations);
4951
return 0;
5052

53+
case "stats":
54+
StatisticsCollector.Run();
55+
return 0;
56+
5157
default:
5258
PrintUsage();
5359
return mode == "help" ? 0 : 1;
@@ -105,6 +111,7 @@ private static void PrintUsage()
105111
Console.WriteLine("Usage:");
106112
Console.WriteLine(" dotnet run -c Release -- benchmark Run BenchmarkDotNet.");
107113
Console.WriteLine(" dotnet run -c Release -- profile [N] Run a plain loop (N iterations, default {0}) suitable for Rider profiling.", DefaultProfileIterations);
114+
Console.WriteLine(" dotnet run -c Release -- stats Collect penalty and mask-pattern statistics (Markdown tables).");
108115
Console.WriteLine();
109116
Console.WriteLine("Tip: attach Rider's dotTrace / dotMemory to the 'profile' process to inspect QrCode.EncodeText hotspots.");
110117
}

QrCodeGeneratorProfiling/README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -255,29 +255,29 @@ Apple M5 Pro, 1 CPU, 18 logical and 18 physical cores
255255

256256
# Penalty Contribution
257257

258-
Penalty contribution statistics (samples=3,200,000)
258+
Penalty contribution statistics (samples=6,400)
259259

260-
| Bucket | Min | Max | Mean | StdDev | Share% |
260+
| Bucket | Min | Max | Mean | StdDev | Share% |
261261
|---------------|----:|-----:|-------:|-------:|-------:|
262-
| 2x2Blocks | 81 | 1797 | 480.56 | 270.35 | 48.25 |
263-
| SameColorCols | 74 | 751 | 226.60 | 106.04 | 22.75 |
264-
| SameColorRows | 70 | 688 | 218.13 | 99.71 | 21.90 |
265-
| FinderRows | 0 | 280 | 36.25 | 41.23 | 3.64 |
266-
| FinderCols | 0 | 320 | 34.44 | 40.11 | 3.46 |
262+
| 2x2Blocks | 45 | 1761 | 444.56 | 270.35 | 53.69 |
263+
| SameColorCols | 8 | 685 | 160.60 | 106.04 | 19.40 |
264+
| SameColorRows | 4 | 622 | 152.13 | 99.71 | 18.37 |
265+
| FinderRows | 0 | 280 | 36.25 | 41.23 | 4.38 |
266+
| FinderCols | 0 | 320 | 34.44 | 40.11 | 4.16 |
267267
| ColorBalance | 0 | 10 | 0.05 | 0.71 | 0.01 |
268268

269269

270270
# Mask Pattern Selection
271271

272-
Mask pattern selection (samples=400,000)
273-
274-
| Pattern | Count | Share% |
275-
|------------|---------:|---------:|
276-
| 2 | 125,000 | 31.25 |
277-
| 3 | 58,500 | 14.62 |
278-
| 7 | 53,500 | 13.38 |
279-
| 4 | 49,500 | 12.38 |
280-
| 6 | 43,500 | 10.88 |
281-
| 5 | 32,500 | 8.12 |
282-
| 0 | 20,000 | 5.00 |
283-
| 1 | 17,500 | 4.38 |
272+
Mask pattern selection (samples=800)
273+
274+
| Pattern | Count | Share% |
275+
|--------:|------:|-------:|
276+
| 2 | 250 | 31.25 |
277+
| 3 | 117 | 14.62 |
278+
| 7 | 109 | 13.63 |
279+
| 4 | 100 | 12.50 |
280+
| 6 | 91 | 11.38 |
281+
| 5 | 62 | 7.75 |
282+
| 0 | 37 | 4.62 |
283+
| 1 | 34 | 4.25 |
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* QR code generator library (.NET)
3+
*
4+
* Copyright (c) Manuel Bleichenbacher (MIT License)
5+
* https://github.com/manuelbl/QrCodeGenerator
6+
*/
7+
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Globalization;
11+
using System.Linq;
12+
using System.Text;
13+
14+
namespace Net.Codecrete.QrCodeGenerator.Profiling;
15+
16+
/// <summary>
17+
/// Collects statistics about penalty contributions and data mask pattern selection
18+
/// across the sample payloads.
19+
/// </summary>
20+
/// <remarks>
21+
/// <para>
22+
/// The penalty score and the selected mask pattern are a deterministic function of
23+
/// (payload, ECC level), so a single pass over the sample data is sufficient — repeating
24+
/// it would only duplicate identical samples without adding information.
25+
/// </para>
26+
/// <para>
27+
/// Passing an <see cref="EncodingInfo"/> forces the library to fully evaluate the penalty
28+
/// score for all eight mask patterns (disabling the early-stop optimisation), which is what
29+
/// makes the per-bucket breakdown available.
30+
/// </para>
31+
/// </remarks>
32+
internal static class StatisticsCollector
33+
{
34+
public static void Run()
35+
{
36+
var payloads = SampleData.Payloads;
37+
var eccLevels = new[] { QrCode.Ecc.Low, QrCode.Ecc.Medium, QrCode.Ecc.Quartile, QrCode.Ecc.High };
38+
39+
// Penalty buckets, named to match the rules in Penalty.CalculateFully.
40+
var blocks = new Bucket("2x2Blocks");
41+
var sameColorCols = new Bucket("SameColorCols");
42+
var sameColorRows = new Bucket("SameColorRows");
43+
var finderRows = new Bucket("FinderRows");
44+
var finderCols = new Bucket("FinderCols");
45+
var colorBalance = new Bucket("ColorBalance");
46+
47+
var maskCounts = new long[8];
48+
49+
foreach (var payload in payloads)
50+
{
51+
foreach (var eccLevel in eccLevels)
52+
{
53+
var info = new EncodingInfo();
54+
var qr = QrCode.EncodeTextAdvanced(payload, eccLevel, encodingInfo: info);
55+
56+
// Penalty statistics cover all eight candidate mask patterns.
57+
foreach (var penalty in info.Penalties)
58+
{
59+
blocks.Add(penalty.Blocks);
60+
sameColorCols.Add(penalty.VerticalStreaks);
61+
sameColorRows.Add(penalty.HorizontalStreaks);
62+
finderRows.Add(penalty.HorizontalFinderPatterns);
63+
finderCols.Add(penalty.VerticalFinderPatterns);
64+
colorBalance.Add(penalty.ColorBalance);
65+
}
66+
67+
// Mask statistics cover the actually selected pattern.
68+
maskCounts[qr.Mask] += 1;
69+
}
70+
}
71+
72+
var buckets = new[] { blocks, sameColorCols, sameColorRows, finderRows, finderCols, colorBalance };
73+
PrintPenaltyTable(buckets);
74+
Console.WriteLine();
75+
PrintMaskTable(maskCounts);
76+
}
77+
78+
private static void PrintPenaltyTable(IReadOnlyList<Bucket> buckets)
79+
{
80+
var sampleCount = buckets[0].Count;
81+
var totalMean = buckets.Sum(b => b.Mean);
82+
83+
Console.WriteLine("# Penalty Contribution");
84+
Console.WriteLine();
85+
Console.WriteLine($"Penalty contribution statistics (samples={sampleCount.ToString("N0", CultureInfo.InvariantCulture)})");
86+
Console.WriteLine();
87+
88+
var headers = new[] { "Bucket", "Min", "Max", "Mean", "StdDev", "Share%" };
89+
var rightAlign = new[] { false, true, true, true, true, true };
90+
var rows = buckets
91+
.OrderByDescending(b => b.Mean)
92+
.Select(b => new[]
93+
{
94+
b.Name,
95+
b.Min.ToString(CultureInfo.InvariantCulture),
96+
b.Max.ToString(CultureInfo.InvariantCulture),
97+
b.Mean.ToString("F2", CultureInfo.InvariantCulture),
98+
b.StdDev.ToString("F2", CultureInfo.InvariantCulture),
99+
(totalMean > 0 ? b.Mean / totalMean * 100 : 0).ToString("F2", CultureInfo.InvariantCulture)
100+
})
101+
.ToList();
102+
103+
PrintTable(headers, rightAlign, rows);
104+
}
105+
106+
private static void PrintMaskTable(long[] maskCounts)
107+
{
108+
var total = maskCounts.Sum();
109+
110+
Console.WriteLine("# Mask Pattern Selection");
111+
Console.WriteLine();
112+
Console.WriteLine($"Mask pattern selection (samples={total.ToString("N0", CultureInfo.InvariantCulture)})");
113+
Console.WriteLine();
114+
115+
var headers = new[] { "Pattern", "Count", "Share%" };
116+
var rightAlign = new[] { true, true, true };
117+
var rows = Enumerable.Range(0, maskCounts.Length)
118+
.OrderByDescending(p => maskCounts[p])
119+
.Select(p => new[]
120+
{
121+
p.ToString(CultureInfo.InvariantCulture),
122+
maskCounts[p].ToString("N0", CultureInfo.InvariantCulture),
123+
(total > 0 ? (double)maskCounts[p] / total * 100 : 0).ToString("F2", CultureInfo.InvariantCulture)
124+
})
125+
.ToList();
126+
127+
PrintTable(headers, rightAlign, rows);
128+
}
129+
130+
private static void PrintTable(string[] headers, bool[] rightAlign, IReadOnlyList<string[]> rows)
131+
{
132+
var widths = new int[headers.Length];
133+
for (var c = 0; c < headers.Length; c += 1)
134+
{
135+
widths[c] = headers[c].Length;
136+
foreach (var row in rows)
137+
{
138+
widths[c] = Math.Max(widths[c], row[c].Length);
139+
}
140+
}
141+
142+
Console.WriteLine(BuildRow(headers, widths, rightAlign));
143+
Console.WriteLine(BuildSeparator(widths, rightAlign));
144+
foreach (var row in rows)
145+
{
146+
Console.WriteLine(BuildRow(row, widths, rightAlign));
147+
}
148+
}
149+
150+
private static string BuildRow(string[] cells, int[] widths, bool[] rightAlign)
151+
{
152+
var builder = new StringBuilder("|");
153+
for (var c = 0; c < cells.Length; c += 1)
154+
{
155+
var cell = rightAlign[c]
156+
? cells[c].PadLeft(widths[c])
157+
: cells[c].PadRight(widths[c]);
158+
builder.Append(' ').Append(cell).Append(" |");
159+
}
160+
return builder.ToString();
161+
}
162+
163+
private static string BuildSeparator(int[] widths, bool[] rightAlign)
164+
{
165+
// Each separator cell spans the column width plus the two padding spaces used
166+
// by BuildRow, with a trailing colon marking right-aligned columns.
167+
var builder = new StringBuilder("|");
168+
for (var c = 0; c < widths.Length; c += 1)
169+
{
170+
if (rightAlign[c])
171+
{
172+
builder.Append(new string('-', widths[c] + 1)).Append(':');
173+
}
174+
else
175+
{
176+
builder.Append(new string('-', widths[c] + 2));
177+
}
178+
builder.Append('|');
179+
}
180+
return builder.ToString();
181+
}
182+
183+
/// <summary>
184+
/// Accumulates count, min, max, mean, and population standard deviation for one penalty bucket.
185+
/// </summary>
186+
private sealed class Bucket
187+
{
188+
private long _sum;
189+
private double _sumSquares;
190+
191+
public Bucket(string name)
192+
{
193+
Name = name;
194+
}
195+
196+
public string Name { get; }
197+
198+
public long Count { get; private set; }
199+
200+
public int Min { get; private set; } = int.MaxValue;
201+
202+
public int Max { get; private set; } = int.MinValue;
203+
204+
public double Mean => Count > 0 ? (double)_sum / Count : 0;
205+
206+
public double StdDev
207+
{
208+
get
209+
{
210+
if (Count == 0)
211+
{
212+
return 0;
213+
}
214+
var mean = Mean;
215+
var variance = _sumSquares / Count - mean * mean;
216+
return Math.Sqrt(Math.Max(0, variance));
217+
}
218+
}
219+
220+
public void Add(int value)
221+
{
222+
Count += 1;
223+
_sum += value;
224+
_sumSquares += (double)value * value;
225+
if (value < Min)
226+
{
227+
Min = value;
228+
}
229+
if (value > Max)
230+
{
231+
Max = value;
232+
}
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)