| @@ -1,4 +1,5 @@ |
| 1 | 1 | const std = ; |
| 2 | +const builtin = ; |
| 2 | 3 | const Regex = .Regex; |
| 3 | 4 | const RegexError = .RegexError; |
| 4 | 5 | |
| @@ -179,3 +180,90 @@ test "regression: double pattern compile and deinit" { |
| 179 | 180 | try std.testing.expect(try regex.isMatch("def")); |
| 180 | 181 | } |
| 181 | 182 | } |
| 183 | + |
| 184 | +// --- findAll quadratic blowup (issue: "findAll is O(n^2), not linear") --- |
| 185 | +// |
| 186 | +// Before the fix, matchAt kept iterating to input.len after all threads died, |
| 187 | +// and findAll restarted matchAt per match, so a scan with m matches over n |
| 188 | +// bytes cost ~O(n*m). The guard below doubles the input and checks the time |
| 189 | +// ratio: a linear scan stays near ~2x, the pre-fix quadratic path was ~4x and |
| 190 | +// growing. A ratio (not an absolute bound) keeps this independent of build |
| 191 | +// mode (Debug vs ReleaseFast) and machine speed. |
| 192 | + |
| 193 | +fn monotonicNs() u64 { |
| 194 | + const clk: std.c.clockid_t = switch (builtin.os.tag) { |
| 195 | + .macos, .ios, .tvos, .watchos, .visionos => .UPTIME_RAW, |
| 196 | + else => .MONOTONIC, |
| 197 | + }; |
| 198 | + var ts: std.c.timespec = undefined; |
| 199 | + _ = std.c.clock_gettime(clk, &ts); |
| 200 | + return @as(u64, @intCast(ts.sec)) * 1_000_000_000 + @as(u64, @intCast(ts.nsec)); |
| 201 | +} |
| 202 | + |
| 203 | +const FindAllScan = struct { ns: u64, count: usize, first_start: usize, last_end: usize }; |
| 204 | + |
| 205 | +fn timeFindAll(allocator: std.mem.Allocator, regex: *const Regex, n: usize) !FindAllScan { |
| 206 | + const buf = try allocator.alloc(u8, n); |
| 207 | + defer allocator.free(buf); |
| 208 | + @memset(buf, '.'); |
| 209 | + var i: usize = 0; |
| 210 | + while (i + 8 <= n) : (i += 64) @memcpy(buf[i .. i + 8], "Sherlock"); |
| 211 | + |
| 212 | + // Warm up (allocator caches, code paths), then take the min of a few runs |
| 213 | + // to suppress scheduler noise without depending on absolute timing. |
| 214 | + { |
| 215 | + const warm = try regex.findAll(allocator, buf); |
| 216 | + for (warm) |*m| { |
| 217 | + var mm = m.*; |
| 218 | + mm.deinit(allocator); |
| 219 | + } |
| 220 | + allocator.free(warm); |
| 221 | + } |
| 222 | + |
| 223 | + var best: u64 = std.math.maxInt(u64); |
| 224 | + var count: usize = 0; |
| 225 | + var first_start: usize = 0; |
| 226 | + var last_end: usize = 0; |
| 227 | + var rep: usize = 0; |
| 228 | + while (rep < 3) : (rep += 1) { |
| 229 | + const t0 = monotonicNs(); |
| 230 | + const matches = try regex.findAll(allocator, buf); |
| 231 | + const dt = monotonicNs() - t0; |
| 232 | + count = matches.len; |
| 233 | + if (matches.len > 0) { |
| 234 | + first_start = matches[0].start; |
| 235 | + last_end = matches[matches.len - 1].end; |
| 236 | + } |
| 237 | + for (matches) |*m| { |
| 238 | + var mm = m.*; |
| 239 | + mm.deinit(allocator); |
| 240 | + } |
| 241 | + allocator.free(matches); |
| 242 | + if (dt < best) best = dt; |
| 243 | + } |
| 244 | + return .{ .ns = best, .count = count, .first_start = first_start, .last_end = last_end }; |
| 245 | +} |
| 246 | + |
| 247 | +test "regression: findAll scales linearly (no O(n^2) blowup)" { |
| 248 | + const allocator = std.testing.allocator; |
| 249 | + |
| 250 | + var regex = try Regex.compile(allocator, "Sherlock"); |
| 251 | + defer regex.deinit(); |
| 252 | + |
| 253 | + const n1: usize = 16 * 1024; |
| 254 | + const n2: usize = 32 * 1024; // exactly 2x |
| 255 | + |
| 256 | + const r1 = try timeFindAll(allocator, ®ex, n1); |
| 257 | + const r2 = try timeFindAll(allocator, ®ex, n2); |
| 258 | + |
| 259 | + // Correctness: a "Sherlock" every 64 bytes, found at the right places. |
| 260 | + try std.testing.expectEqual(n1 / 64, r1.count); |
| 261 | + try std.testing.expectEqual(n2 / 64, r2.count); |
| 262 | + try std.testing.expectEqual(@as(usize, 0), r2.first_start); |
| 263 | + try std.testing.expectEqual(((n2 - 8) / 64) * 64 + 8, r2.last_end); |
| 264 | + |
| 265 | + // Linearity guard: doubling input must not more-than-triple the time. |
| 266 | + // Linear ≈ 2x; the pre-fix quadratic path was ≈ 4x (and worsening). |
| 267 | + try std.testing.expect(r1.ns > 0); |
| 268 | + try std.testing.expect(r2.ns * 10 < r1.ns * 30); // r2/r1 < 3.0 |
| 269 | +} |