|
| 1 | +//go:build !windows |
| 2 | + |
| 3 | +package profile_test |
| 4 | + |
| 5 | +import ( |
| 6 | + "testing" |
| 7 | + |
| 8 | + "github.com/consensys/gnark-crypto/ecc" |
| 9 | + "github.com/consensys/gnark/frontend" |
| 10 | + "github.com/consensys/gnark/frontend/cs/r1cs" |
| 11 | + "github.com/consensys/gnark/frontend/cs/scs" |
| 12 | + "github.com/consensys/gnark/profile" |
| 13 | + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" |
| 14 | + "github.com/consensys/gnark/std/evmprecompiles" |
| 15 | + "github.com/consensys/gnark/std/math/emulated" |
| 16 | +) |
| 17 | + |
| 18 | +// EmulatedCircuit performs emulated arithmetic which uses deferred constraints |
| 19 | +type EmulatedCircuit struct { |
| 20 | + A, B emulated.Element[emulated.Secp256k1Fp] |
| 21 | +} |
| 22 | + |
| 23 | +func (c *EmulatedCircuit) Define(api frontend.API) error { |
| 24 | + f, err := emulated.NewField[emulated.Secp256k1Fp](api) |
| 25 | + if err != nil { |
| 26 | + return err |
| 27 | + } |
| 28 | + |
| 29 | + // These operations use deferred constraint creation |
| 30 | + // Without operation profiling, profiling wouldn't show where Mul was called |
| 31 | + res := f.Mul(&c.A, &c.B) |
| 32 | + res = f.Mul(res, &c.A) |
| 33 | + f.AssertIsEqual(res, &c.B) |
| 34 | + |
| 35 | + return nil |
| 36 | +} |
| 37 | + |
| 38 | +func TestOperations(t *testing.T) { |
| 39 | + p := profile.Start(profile.WithNoOutput()) |
| 40 | + _, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &EmulatedCircuit{}) |
| 41 | + if err != nil { |
| 42 | + t.Fatal(err) |
| 43 | + } |
| 44 | + p.Stop() |
| 45 | + |
| 46 | + // Check that we recorded both constraint and operation samples |
| 47 | + nbConstraints := p.NbConstraints() |
| 48 | + nbOperations := p.NbOperations() |
| 49 | + |
| 50 | + if nbConstraints == 0 { |
| 51 | + t.Error("expected some constraints to be recorded") |
| 52 | + } |
| 53 | + |
| 54 | + if nbOperations == 0 { |
| 55 | + t.Error("expected some operations to be recorded") |
| 56 | + } |
| 57 | + |
| 58 | + // We did 2 Mul operations and 1 AssertIsEqual (which calls checkZero) |
| 59 | + // So we expect at least 3 operations |
| 60 | + // (there might be more due to internal operations like Reduce) |
| 61 | + if nbOperations < 3 { |
| 62 | + t.Errorf("expected at least 3 operations (2 Mul + 1 AssertIsEqual), got %d", nbOperations) |
| 63 | + } |
| 64 | + |
| 65 | + t.Logf("Constraints: %d, Operations: %d", nbConstraints, nbOperations) |
| 66 | + t.Logf("\n--- Constraints (sample_index=0) ---\n%s", p.Top()) |
| 67 | + t.Logf("\n--- Operations (sample_index=1) ---\n%s", p.TopOperations()) |
| 68 | +} |
| 69 | + |
| 70 | +func TestOperationsDirectAPI(t *testing.T) { |
| 71 | + // Test the RecordOperation API directly |
| 72 | + p := profile.Start(profile.WithNoOutput()) |
| 73 | + |
| 74 | + // Record some operations manually (each records count=1) |
| 75 | + profile.RecordOperation("test.op1", 1) |
| 76 | + profile.RecordOperation("test.op2", 1) |
| 77 | + profile.RecordOperation("test.op2", 1) // record op2 twice |
| 78 | + |
| 79 | + // Also record regular constraints (simulate with RecordConstraint) |
| 80 | + profile.RecordConstraint() |
| 81 | + profile.RecordConstraint() |
| 82 | + |
| 83 | + p.Stop() |
| 84 | + |
| 85 | + nbConstraints := p.NbConstraints() |
| 86 | + nbOperations := p.NbOperations() |
| 87 | + |
| 88 | + if nbConstraints != 2 { |
| 89 | + t.Errorf("expected 2 constraints, got %d", nbConstraints) |
| 90 | + } |
| 91 | + |
| 92 | + if nbOperations != 3 { |
| 93 | + t.Errorf("expected 3 operations (1+2), got %d", nbOperations) |
| 94 | + } |
| 95 | + |
| 96 | + t.Logf("Direct API test - Constraints: %d, Operations: %d", nbConstraints, nbOperations) |
| 97 | +} |
| 98 | + |
| 99 | +func TestOperationsNoSession(t *testing.T) { |
| 100 | + // When no profiling session is active, RecordOperation should be a no-op |
| 101 | + // This tests that it doesn't panic and doesn't affect anything |
| 102 | + |
| 103 | + // No profile.Start() - just call RecordOperation |
| 104 | + profile.RecordOperation("test.noop", 1) |
| 105 | + profile.RecordConstraint() |
| 106 | + |
| 107 | + // Start a new session to verify nothing was recorded |
| 108 | + p := profile.Start(profile.WithNoOutput()) |
| 109 | + p.Stop() |
| 110 | + |
| 111 | + // Should have 0 since we started the session after the calls |
| 112 | + if p.NbConstraints() != 0 { |
| 113 | + t.Errorf("expected 0 constraints when session started after recording") |
| 114 | + } |
| 115 | + if p.NbOperations() != 0 { |
| 116 | + t.Errorf("expected 0 operations when session started after recording") |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +func TestOperationWeights(t *testing.T) { |
| 121 | + // Test that WithOperationWeights multiplies counts correctly |
| 122 | + weights := map[string]int{ |
| 123 | + "expensive.op": 10, |
| 124 | + "medium.op": 5, |
| 125 | + // "cheap.op" is not in the map, should use count=1 |
| 126 | + } |
| 127 | + p := profile.Start(profile.WithNoOutput(), profile.WithOperationWeights(weights)) |
| 128 | + |
| 129 | + // Record operations |
| 130 | + profile.RecordOperation("expensive.op", 1) // should count as 10 |
| 131 | + profile.RecordOperation("medium.op", 1) // should count as 5 |
| 132 | + profile.RecordOperation("medium.op", 1) // should count as 5 |
| 133 | + profile.RecordOperation("cheap.op", 1) // should count as 1 (no weight) |
| 134 | + |
| 135 | + p.Stop() |
| 136 | + |
| 137 | + nbOperations := p.NbOperations() |
| 138 | + // Expected: 10 + 5 + 5 + 1 = 21 |
| 139 | + if nbOperations != 21 { |
| 140 | + t.Errorf("expected 21 operations with weights (10+5+5+1), got %d", nbOperations) |
| 141 | + } |
| 142 | + |
| 143 | + t.Logf("WithOperationWeights test - Operations: %d (expected 21)", nbOperations) |
| 144 | +} |
| 145 | + |
| 146 | +func TestOperationWeightsMultipleSessions(t *testing.T) { |
| 147 | + // Test that different sessions can have different weights |
| 148 | + weights1 := map[string]int{"op": 10} |
| 149 | + weights2 := map[string]int{"op": 2} |
| 150 | + |
| 151 | + p1 := profile.Start(profile.WithNoOutput(), profile.WithOperationWeights(weights1)) |
| 152 | + p2 := profile.Start(profile.WithNoOutput(), profile.WithOperationWeights(weights2)) |
| 153 | + |
| 154 | + // Record an operation - each session should apply its own weight |
| 155 | + profile.RecordOperation("op", 1) |
| 156 | + |
| 157 | + p2.Stop() |
| 158 | + p1.Stop() |
| 159 | + |
| 160 | + if p1.NbOperations() != 10 { |
| 161 | + t.Errorf("session 1: expected 10 operations, got %d", p1.NbOperations()) |
| 162 | + } |
| 163 | + if p2.NbOperations() != 2 { |
| 164 | + t.Errorf("session 2: expected 2 operations, got %d", p2.NbOperations()) |
| 165 | + } |
| 166 | + |
| 167 | + t.Logf("Multiple sessions - p1: %d (expected 10), p2: %d (expected 2)", |
| 168 | + p1.NbOperations(), p2.NbOperations()) |
| 169 | +} |
| 170 | + |
| 171 | +// ECMulCircuit wraps the ECMul precompile for profiling |
| 172 | +type ECMulCircuit struct { |
| 173 | + P sw_emulated.AffinePoint[emulated.BN254Fp] |
| 174 | + U emulated.Element[emulated.BN254Fr] |
| 175 | +} |
| 176 | + |
| 177 | +func (c *ECMulCircuit) Define(api frontend.API) error { |
| 178 | + // This is a complex operation that involves many emulated field operations |
| 179 | + // and range checks - perfect for demonstrating operation profiling |
| 180 | + _ = evmprecompiles.ECMul(api, &c.P, &c.U) |
| 181 | + return nil |
| 182 | +} |
| 183 | + |
| 184 | +func TestOperationsECMul(t *testing.T) { |
| 185 | + // Test with a more complex circuit - ECMul precompile |
| 186 | + p := profile.Start(profile.WithNoOutput()) |
| 187 | + _, err := frontend.Compile(ecc.BN254.ScalarField(), scs.NewBuilder, &ECMulCircuit{}) |
| 188 | + if err != nil { |
| 189 | + t.Fatal(err) |
| 190 | + } |
| 191 | + p.Stop() |
| 192 | + |
| 193 | + nbConstraints := p.NbConstraints() |
| 194 | + nbOperations := p.NbOperations() |
| 195 | + |
| 196 | + t.Logf("ECMul Circuit - Constraints: %d, Operations: %d", nbConstraints, nbOperations) |
| 197 | + |
| 198 | + // ECMul involves many multiplications and range checks |
| 199 | + // We expect a significant number of operations |
| 200 | + if nbOperations < 100 { |
| 201 | + t.Errorf("expected at least 100 operations for ECMul, got %d", nbOperations) |
| 202 | + } |
| 203 | + |
| 204 | + // Print operations tree to show the breakdown |
| 205 | + t.Logf("\n--- Operations Breakdown ---\n%s", p.TopOperations()) |
| 206 | +} |
| 207 | + |
| 208 | +// Example_operations demonstrates how to use operation profiling |
| 209 | +// with emulated arithmetic. |
| 210 | +func Example_operations() { |
| 211 | + // Start profiling - operations will be tracked at call sites |
| 212 | + p := profile.Start(profile.WithNoOutput()) |
| 213 | + |
| 214 | + // Compile a circuit using emulated arithmetic |
| 215 | + _, _ = frontend.Compile(ecc.BN254.ScalarField(), scs.NewBuilder, &EmulatedCircuit{}) |
| 216 | + |
| 217 | + p.Stop() |
| 218 | + |
| 219 | + // View actual constraints (default) |
| 220 | + // go tool pprof -sample_index=0 gnark.pprof |
| 221 | + |
| 222 | + // View operations (shows where Mul/AssertIsEqual were called) |
| 223 | + // go tool pprof -sample_index=1 gnark.pprof |
| 224 | + |
| 225 | + // Or programmatically: |
| 226 | + // p.Top() - shows constraint tree |
| 227 | + // p.TopOperations() - shows operation tree |
| 228 | +} |
| 229 | + |
| 230 | +func TestWithoutOperations(t *testing.T) { |
| 231 | + // Test that WithoutOperations excludes operation samples from the profile |
| 232 | + p := profile.Start(profile.WithNoOutput(), profile.WithoutOperations()) |
| 233 | + |
| 234 | + // Record both constraint and operation samples |
| 235 | + profile.RecordConstraint() |
| 236 | + profile.RecordConstraint() |
| 237 | + profile.RecordOperation("test.op", 1) |
| 238 | + profile.RecordOperation("test.op", 1) |
| 239 | + |
| 240 | + p.Stop() |
| 241 | + |
| 242 | + // After Stop(), filtering is applied |
| 243 | + // NbConstraints should still work (returns 2) |
| 244 | + nbConstraints := p.NbConstraints() |
| 245 | + if nbConstraints != 2 { |
| 246 | + t.Errorf("expected 2 constraints, got %d", nbConstraints) |
| 247 | + } |
| 248 | + |
| 249 | + // Operations count should be 0 after filtering |
| 250 | + nbOperations := p.NbOperations() |
| 251 | + if nbOperations != 0 { |
| 252 | + t.Errorf("expected 0 operations with WithoutOperations, got %d", nbOperations) |
| 253 | + } |
| 254 | + |
| 255 | + t.Logf("WithoutOperations - Constraints: %d, Operations: %d", nbConstraints, nbOperations) |
| 256 | +} |
| 257 | + |
| 258 | +func TestWithoutConstraints(t *testing.T) { |
| 259 | + // Test that WithoutConstraints excludes constraint samples from the profile |
| 260 | + p := profile.Start(profile.WithNoOutput(), profile.WithoutConstraints()) |
| 261 | + |
| 262 | + // Record both constraint and operation samples |
| 263 | + profile.RecordConstraint() |
| 264 | + profile.RecordConstraint() |
| 265 | + profile.RecordOperation("test.op", 1) |
| 266 | + profile.RecordOperation("test.op", 1) |
| 267 | + |
| 268 | + p.Stop() |
| 269 | + |
| 270 | + // NbConstraints returns 0 when WithoutConstraints is used |
| 271 | + nbConstraints := p.NbConstraints() |
| 272 | + if nbConstraints != 0 { |
| 273 | + t.Errorf("expected 0 constraints with WithoutConstraints, got %d", nbConstraints) |
| 274 | + } |
| 275 | + |
| 276 | + // NbOperations should still work correctly |
| 277 | + nbOperations := p.NbOperations() |
| 278 | + if nbOperations != 2 { |
| 279 | + t.Errorf("expected 2 operations, got %d", nbOperations) |
| 280 | + } |
| 281 | + |
| 282 | + // Top() should return empty string when constraints are excluded |
| 283 | + if p.Top() != "" { |
| 284 | + t.Errorf("expected empty Top() with WithoutConstraints") |
| 285 | + } |
| 286 | + |
| 287 | + // TopOperations() should still work |
| 288 | + if p.TopOperations() == "" { |
| 289 | + t.Errorf("expected non-empty TopOperations() with WithoutConstraints") |
| 290 | + } |
| 291 | + |
| 292 | + t.Logf("WithoutConstraints - Constraints: %d, Operations: %d", nbConstraints, nbOperations) |
| 293 | +} |
| 294 | + |
| 295 | +func TestWithoutBoth(t *testing.T) { |
| 296 | + // Test that using both WithoutConstraints and WithoutOperations panics |
| 297 | + defer func() { |
| 298 | + if r := recover(); r == nil { |
| 299 | + t.Errorf("expected panic when using both WithoutConstraints and WithoutOperations") |
| 300 | + } |
| 301 | + }() |
| 302 | + |
| 303 | + _ = profile.Start(profile.WithNoOutput(), profile.WithoutConstraints(), profile.WithoutOperations()) |
| 304 | +} |
0 commit comments