Skip to content

Commit 7c23f06

Browse files
gbotrelivokub
andauthored
feat: add new api to profile package to capture virtual constraints (#1696)
Co-authored-by: Ivo Kubjas <ivo.kubjas@consensys.net>
1 parent c44625b commit 7c23f06

File tree

11 files changed

+710
-13
lines changed

11 files changed

+710
-13
lines changed

profile/operation_test.go

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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

Comments
 (0)