-
Notifications
You must be signed in to change notification settings - Fork 0
perf(scale): optimize scale read loop lock contention #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a3e5a6f
4496e8c
5dd8976
10186b8
11f3548
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package scale | ||
|
|
||
| import ( | ||
| "context" | ||
| "io" | ||
| "sync" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/adcondev/scale-daemon/internal/config" | ||
| "go.bug.st/serial" | ||
| ) | ||
|
|
||
| // MockPort implements Port interface for testing | ||
| type MockPort struct { | ||
| mu sync.Mutex | ||
| closed bool | ||
| } | ||
|
|
||
| func (m *MockPort) Read(p []byte) (n int, err error) { | ||
| m.mu.Lock() | ||
| defer m.mu.Unlock() | ||
| if m.closed { | ||
| return 0, io.EOF | ||
| } | ||
| // Simulate some data | ||
| return copy(p, []byte("10.50")), nil | ||
| } | ||
|
|
||
| func (m *MockPort) Write(p []byte) (n int, err error) { | ||
| m.mu.Lock() | ||
| defer m.mu.Unlock() | ||
| if m.closed { | ||
| return 0, io.ErrClosedPipe | ||
| } | ||
| return len(p), nil | ||
| } | ||
|
|
||
| func (m *MockPort) Close() error { | ||
| m.mu.Lock() | ||
| defer m.mu.Unlock() | ||
| m.closed = true | ||
| return nil | ||
| } | ||
|
|
||
| func (m *MockPort) SetReadTimeout(t time.Duration) error { | ||
| return nil | ||
| } | ||
|
|
||
| func TestLockContention(t *testing.T) { | ||
| // Setup config | ||
| cfg := config.New(config.Environment{ | ||
| DefaultPort: "COM_TEST", | ||
| DefaultMode: false, // Ensure we use the real connection path | ||
| }) | ||
|
|
||
| // Override serialOpen for testing | ||
| origSerialOpen := serialOpen | ||
| defer func() { serialOpen = origSerialOpen }() | ||
|
|
||
| mockPort := &MockPort{} | ||
| serialOpen = func(name string, mode *serial.Mode) (Port, error) { | ||
| return mockPort, nil | ||
| } | ||
|
|
||
| broadcast := make(chan string, 10) | ||
| r := NewReader(cfg, broadcast) | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| // Start reader in background | ||
| go r.Start(ctx) | ||
|
|
||
|
Comment on lines
+69
to
+79
|
||
| // Wait for the reader to enter the read loop and acquire the lock. | ||
| // We want to catch it during the 500ms sleep. | ||
| // Since we don't know exactly when it starts sleeping, we can try multiple times or just wait a bit. | ||
| // Connect happens fast (mock). Write happens fast (mock). | ||
| // So sleep starts almost immediately. | ||
| time.Sleep(50 * time.Millisecond) | ||
|
|
||
| // Now try to close port. This acquires the lock. | ||
| start := time.Now() | ||
|
|
||
| // r.ClosePort() will block until the lock is released. | ||
| // If the lock is held during sleep, this will take ~450ms (500ms - 50ms). | ||
| r.ClosePort() | ||
|
|
||
| duration := time.Since(start) | ||
|
|
||
| t.Logf("ClosePort duration: %v", duration) | ||
|
|
||
| if duration > 100*time.Millisecond { | ||
| t.Errorf("Lock contention detected: ClosePort took %v, expected < 100ms. The lock is likely held during sleep.", duration) | ||
| } | ||
|
Comment on lines
+74
to
+100
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test mutates the package-level
serialOpenvariable. If any other tests in packagescaleare later markedt.Parallel()(or another test also overridesserialOpen), this will introduce data races and nondeterministic behavior. Consider protectingserialOpenoverrides with a mutex in tests, or redesigningNewReader/Readerto accept the opener dependency so each test instance can inject it without touching global state.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jules check this out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jules here is more context