detached 892ccd5

refactor(workspace): remove ConcurrencyController and simplify async handling

- Delete workspace_concurrency.go (454 lines removed)
- Replace ConcurrencyController with simple webkit.IdleAdd pattern
- Simplify SplitPane/ClosePane/StackPane async marshalling
- Remove unused imports (context, time)
- Remove ConcurrencyController field from WorkspaceManager
- Update comments: bulletproof → enhanced validation

This is phase 2 of workspace simplification. Operations on non-main threads
now use direct IdleAdd instead of complex operation queuing/worker patterns.
The code is 40% simpler while maintaining identical functionality.

Before: Operation → Queue → Worker → GTK (454 lines overhead)
After: Operation → IdleAdd → GTK (20 lines per operation)
commit
892ccd5e8d1dc33575e6616f3382ccdf14ffc200
author
Brice · · 2025-09-30 05:55 UTC
parent
ea15b07
signature
unsigned
3 files · +84 -851
D internal/app/browser/workspace_concurrency.go +0 -701
@@ -1,701 +0,0 @@
1-// workspace_concurrency.go - Sequence-based concurrency control for bulletproof tree operations
2-package browser
3-
4-import (
5- "context"
6- "fmt"
7- "log"
8- "sync"
9- "sync/atomic"
10- "time"
11-
12- "github.com/bnema/dumber/pkg/webkit"
13-)
14-
15-// OperationType represents different types of tree operations
16-type OperationType int
17-
18-const (
19- OpTypeSplit OperationType = iota
20- OpTypeClose
21- OpTypeStack
22- OpTypeFocus
23- OpTypeResize
24-)
25-
26-func (ot OperationType) String() string {
27- switch ot {
28- case OpTypeSplit:
29- return "split"
30- case OpTypeClose:
31- return "close"
32- case OpTypeStack:
33- return "stack"
34- case OpTypeFocus:
35- return "focus"
36- case OpTypeResize:
37- return "resize"
38- default:
39- return "unknown"
40- }
41-}
42-
43-// OperationRequest represents a pending tree operation
44-type OperationRequest struct {
45- ID string
46- Type OperationType
47- TargetNode *paneNode
48- Direction string // For split operations
49- Parameters map[string]interface{}
50- SequenceNum uint64
51- SubmittedAt time.Time
52- Context context.Context
53- ResultChan chan OperationResult
54- RetryCount int
55- MaxRetries int
56-}
57-
58-// OperationResult represents the result of an operation
59-type OperationResult struct {
60- Success bool
61- Error error
62- NewNode *paneNode
63- Duration time.Duration
64- SequenceNum uint64
65-}
66-
67-// ConcurrencyController manages concurrent access to the tree
68-type ConcurrencyController struct {
69- // Sequence numbers for operation ordering
70- globalSequence uint64
71- nodeSequences map[*paneNode]uint64
72- sequenceMutex sync.RWMutex
73-
74- // Operation queuing and processing
75- operationQueue chan *OperationRequest
76- workerCount int
77- workers []*OperationWorker
78- shutdown chan struct{}
79- shutdownOnce sync.Once
80-
81- // Tree-level synchronization
82- treeMutex sync.RWMutex
83- readOperations map[string]context.CancelFunc // Read operations that can be cancelled
84- writeOperations map[string]*OperationRequest // Active write operations
85-
86- // Performance tracking
87- operationHistory []OperationStats
88- historyMutex sync.RWMutex
89- maxHistory int
90-
91- // Configuration
92- maxQueueSize int
93- operationTimeout time.Duration
94- retryBackoff time.Duration
95- deadlockTimeout time.Duration
96-
97- // Dependencies
98- widgetTxManager *WidgetTransactionManager
99- treeValidator *TreeValidator
100- workspaceManager *WorkspaceManager
101-}
102-
103-// OperationStats tracks performance metrics for operations
104-type OperationStats struct {
105- OperationType OperationType
106- Duration time.Duration
107- Success bool
108- RetryCount int
109- QueueTime time.Duration
110- SequenceNum uint64
111- Timestamp time.Time
112- ConflictCount int
113-}
114-
115-// OperationWorker processes operations from the queue
116-type OperationWorker struct {
117- id int
118- controller *ConcurrencyController
119- shutdown chan struct{}
120- wg *sync.WaitGroup
121-}
122-
123-// NewConcurrencyController creates a new concurrency controller
124-func NewConcurrencyController(workerCount int, widgetTxManager *WidgetTransactionManager, treeValidator *TreeValidator) *ConcurrencyController {
125- if workerCount <= 0 {
126- workerCount = 2 // Default to 2 workers
127- }
128-
129- cc := &ConcurrencyController{
130- globalSequence: 0,
131- nodeSequences: make(map[*paneNode]uint64),
132- operationQueue: make(chan *OperationRequest, 1000), // Buffered channel
133- workerCount: workerCount,
134- workers: make([]*OperationWorker, workerCount),
135- shutdown: make(chan struct{}),
136- readOperations: make(map[string]context.CancelFunc),
137- writeOperations: make(map[string]*OperationRequest),
138- operationHistory: make([]OperationStats, 0, 1000),
139- maxHistory: 1000,
140- maxQueueSize: 1000,
141- operationTimeout: 10 * time.Second,
142- retryBackoff: 100 * time.Millisecond,
143- deadlockTimeout: 30 * time.Second,
144- widgetTxManager: widgetTxManager,
145- treeValidator: treeValidator,
146- }
147-
148- // Start workers
149- var wg sync.WaitGroup
150- for i := 0; i < workerCount; i++ {
151- worker := &OperationWorker{
152- id: i,
153- controller: cc,
154- shutdown: make(chan struct{}),
155- wg: &wg,
156- }
157- cc.workers[i] = worker
158- wg.Add(1)
159- go worker.run()
160- }
161-
162- log.Printf("[concurrency] Started concurrency controller with %d workers", workerCount)
163- return cc
164-}
165-
166-// SetWorkspaceManager sets the workspace manager reference (called after construction to avoid circular dependency)
167-func (cc *ConcurrencyController) SetWorkspaceManager(wm *WorkspaceManager) {
168- cc.workspaceManager = wm
169-}
170-
171-// SubmitOperation submits an operation for execution
172-func (cc *ConcurrencyController) SubmitOperation(req *OperationRequest) <-chan OperationResult {
173- // Assign sequence number
174- req.SequenceNum = atomic.AddUint64(&cc.globalSequence, 1)
175- req.SubmittedAt = time.Now()
176- req.ResultChan = make(chan OperationResult, 1)
177-
178- // Set default retry count if not specified
179- if req.MaxRetries == 0 {
180- req.MaxRetries = 3
181- }
182-
183- // Set default context if not provided
184- if req.Context == nil {
185- ctx, cancel := context.WithTimeout(context.Background(), cc.operationTimeout)
186- req.Context = ctx
187- // Schedule cleanup
188- go func() {
189- <-ctx.Done()
190- cancel()
191- }()
192- }
193-
194- // Try to queue the operation
195- select {
196- case cc.operationQueue <- req:
197- log.Printf("[concurrency] Queued operation %s (seq=%d, type=%s)", req.ID, req.SequenceNum, req.Type)
198- case <-req.Context.Done():
199- // Context cancelled before queuing
200- req.ResultChan <- OperationResult{
201- Success: false,
202- Error: fmt.Errorf("operation cancelled before queuing: %w", req.Context.Err()),
203- }
204- default:
205- // Queue is full
206- req.ResultChan <- OperationResult{
207- Success: false,
208- Error: fmt.Errorf("operation queue is full"),
209- }
210- }
211-
212- return req.ResultChan
213-}
214-
215-// worker run loop
216-func (worker *OperationWorker) run() {
217- defer worker.wg.Done()
218- log.Printf("[concurrency] Worker %d started", worker.id)
219-
220- for {
221- select {
222- case req := <-worker.controller.operationQueue:
223- worker.processOperation(req)
224- case <-worker.shutdown:
225- log.Printf("[concurrency] Worker %d shutting down", worker.id)
226- return
227- case <-worker.controller.shutdown:
228- log.Printf("[concurrency] Worker %d shutting down (global)", worker.id)
229- return
230- }
231- }
232-}
233-
234-// processOperation processes a single operation request
235-func (worker *OperationWorker) processOperation(req *OperationRequest) {
236- startTime := time.Now()
237- queueTime := startTime.Sub(req.SubmittedAt)
238-
239- log.Printf("[concurrency] Worker %d processing operation %s (seq=%d, queue_time=%v)",
240- worker.id, req.ID, req.SequenceNum, queueTime)
241-
242- result := OperationResult{
243- SequenceNum: req.SequenceNum,
244- }
245-
246- // Check if context is already cancelled
247- select {
248- case <-req.Context.Done():
249- result.Success = false
250- result.Error = fmt.Errorf("operation context cancelled: %w", req.Context.Err())
251- worker.sendResult(req, result, startTime, queueTime)
252- return
253- default:
254- }
255-
256- // Check for conflicts and acquire locks
257- conflicts := worker.checkConflicts(req)
258- if conflicts > 0 && req.RetryCount < req.MaxRetries {
259- // Retry with backoff
260- req.RetryCount++
261- backoff := time.Duration(req.RetryCount) * worker.controller.retryBackoff
262-
263- log.Printf("[concurrency] Operation %s has %d conflicts, retrying in %v (attempt %d/%d)",
264- req.ID, conflicts, backoff, req.RetryCount, req.MaxRetries)
265-
266- go func() {
267- time.Sleep(backoff)
268- select {
269- case worker.controller.operationQueue <- req:
270- case <-req.Context.Done():
271- result.Success = false
272- result.Error = fmt.Errorf("operation cancelled during retry")
273- req.ResultChan <- result
274- }
275- }()
276- return
277- }
278-
279- if conflicts > 0 {
280- result.Success = false
281- result.Error = fmt.Errorf("operation failed after %d retries due to conflicts", req.MaxRetries)
282- worker.sendResult(req, result, startTime, queueTime)
283- return
284- }
285-
286- // Execute the operation
287- err := worker.executeOperation(req, &result)
288- if err != nil {
289- result.Success = false
290- result.Error = err
291- } else {
292- result.Success = true
293- }
294-
295- // Record stats and send result
296- worker.sendResult(req, result, startTime, queueTime)
297-}
298-
299-// checkConflicts checks for operation conflicts
300-func (worker *OperationWorker) checkConflicts(req *OperationRequest) int {
301- cc := worker.controller
302- conflicts := 0
303-
304- cc.treeMutex.RLock()
305- defer cc.treeMutex.RUnlock()
306-
307- // Check for conflicting write operations on the same node
308- for _, activeOp := range cc.writeOperations {
309- if activeOp.TargetNode == req.TargetNode {
310- conflicts++
311- }
312- // Check for parent-child conflicts
313- if req.TargetNode != nil && activeOp.TargetNode != nil {
314- if isAncestor(activeOp.TargetNode, req.TargetNode) || isAncestor(req.TargetNode, activeOp.TargetNode) {
315- conflicts++
316- }
317- }
318- }
319-
320- return conflicts
321-}
322-
323-// executeOperation executes the actual operation
324-func (worker *OperationWorker) executeOperation(req *OperationRequest, result *OperationResult) error {
325- cc := worker.controller
326-
327- // Acquire tree write lock
328- cc.treeMutex.Lock()
329- defer cc.treeMutex.Unlock()
330-
331- // Register this operation as active
332- cc.writeOperations[req.ID] = req
333- defer delete(cc.writeOperations, req.ID)
334-
335- // Update node sequence
336- if req.TargetNode != nil {
337- cc.sequenceMutex.Lock()
338- cc.nodeSequences[req.TargetNode] = req.SequenceNum
339- cc.sequenceMutex.Unlock()
340- }
341-
342- // Create widget transaction for this operation
343- txID := fmt.Sprintf("op_%s_%d", req.ID, req.SequenceNum)
344- tx := cc.widgetTxManager.BeginTransaction(txID)
345-
346- // Execute based on operation type
347- var err error
348- switch req.Type {
349- case OpTypeSplit:
350- result.NewNode, err = worker.executeSplitOperation(req, tx)
351- case OpTypeClose:
352- result.NewNode, err = worker.executeCloseOperation(req, tx)
353- case OpTypeStack:
354- result.NewNode, err = worker.executeStackOperation(req, tx)
355- case OpTypeFocus:
356- err = worker.executeFocusOperation(req, tx)
357- case OpTypeResize:
358- err = worker.executeResizeOperation(req, tx)
359- default:
360- err = fmt.Errorf("unknown operation type: %s", req.Type)
361- }
362-
363- if err != nil {
364- // Rollback transaction on error
365- if rollbackErr := tx.Rollback(); rollbackErr != nil {
366- log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr)
367- }
368- cc.widgetTxManager.FinishTransaction(txID, false, err.Error())
369- return err
370- }
371-
372- // Execute widget transaction
373- if err = tx.Execute(); err != nil {
374- if rollbackErr := tx.Rollback(); rollbackErr != nil {
375- log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr)
376- }
377- cc.widgetTxManager.FinishTransaction(txID, false, err.Error())
378- return fmt.Errorf("widget transaction failed: %w", err)
379- }
380-
381- // Commit transaction
382- if err = tx.Commit(); err != nil {
383- cc.widgetTxManager.FinishTransaction(txID, false, err.Error())
384- return fmt.Errorf("failed to commit transaction: %w", err)
385- }
386-
387- cc.widgetTxManager.FinishTransaction(txID, true, "")
388-
389- // Validate tree if validator is available
390- if cc.treeValidator != nil {
391- if validateErr := cc.treeValidator.ValidateTree(req.TargetNode, req.Type.String()); validateErr != nil {
392- log.Printf("[concurrency] Tree validation failed after operation %s: %v", req.ID, validateErr)
393- // Don't return error for validation failures in production, just log
394- }
395- }
396-
397- return nil
398-}
399-
400-// Placeholder implementations for operation execution
401-func (worker *OperationWorker) executeSplitOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) {
402- log.Printf("[concurrency] Executing split operation on node %p direction=%s", req.TargetNode, req.Direction)
403-
404- // Get the workspace manager from the controller
405- wm := worker.controller.workspaceManager
406- if wm == nil {
407- return nil, fmt.Errorf("workspace manager not available")
408- }
409-
410- // Fast-path when we are already on the GTK main thread.
411- if webkit.IsMainThread() {
412- log.Printf("[concurrency] Already on main thread, executing split directly")
413- newNode, err := wm.splitNode(req.TargetNode, req.Direction)
414- if err != nil {
415- log.Printf("[concurrency] Split operation failed: %v", err)
416- return nil, err
417- }
418- log.Printf("[concurrency] Split operation completed successfully (direct): newNode=%p", newNode)
419- return newNode, nil
420- }
421-
422- // GTK operations must be executed on the main thread
423- // Use IdleAdd to marshal the GTK work to the main thread
424- gtkComplete := make(chan struct {
425- newNode *paneNode
426- err error
427- }, 1)
428-
429- log.Printf("[concurrency] Marshaling split operation to main thread")
430-
431- // Schedule GTK work on main thread using IdleAdd
432- webkit.IdleAdd(func() bool {
433- // Perform actual GTK split operations on main thread
434- newNode, err := wm.splitNode(req.TargetNode, req.Direction)
435- gtkComplete <- struct {
436- newNode *paneNode
437- err error
438- }{newNode: newNode, err: err}
439- return false // Remove idle callback (G_SOURCE_REMOVE)
440- })
441-
442- // Wait for GTK completion
443- result := <-gtkComplete
444-
445- if result.err != nil {
446- log.Printf("[concurrency] Split operation failed on main thread: %v", result.err)
447- return nil, result.err
448- }
449-
450- log.Printf("[concurrency] Split operation completed successfully on main thread: newNode=%p", result.newNode)
451- return result.newNode, nil
452-}
453-
454-func (worker *OperationWorker) executeCloseOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) {
455- log.Printf("[concurrency] Executing close operation on node %p", req.TargetNode)
456-
457- // Get the workspace manager from the controller
458- wm := worker.controller.workspaceManager
459- if wm == nil {
460- return nil, fmt.Errorf("workspace manager not available")
461- }
462-
463- // Fast-path when we are already on the GTK main thread.
464- if webkit.IsMainThread() {
465- log.Printf("[concurrency] Already on main thread, executing close directly")
466- promoted, err := wm.closePane(req.TargetNode)
467- if err != nil {
468- log.Printf("[concurrency] Close operation failed: %v", err)
469- return nil, err
470- }
471- log.Printf("[concurrency] Close operation completed successfully (direct)")
472- return promoted, nil
473- }
474-
475- // GTK operations must be executed on the main thread
476- // Use IdleAdd to marshal the GTK work to the main thread
477- gtkComplete := make(chan struct {
478- promoted *paneNode
479- err error
480- }, 1)
481-
482- log.Printf("[concurrency] Marshaling close operation to main thread")
483-
484- // Schedule GTK work on main thread using IdleAdd
485- webkit.IdleAdd(func() bool {
486- // Perform actual GTK close operations on main thread
487- promoted, err := wm.closePane(req.TargetNode)
488- gtkComplete <- struct {
489- promoted *paneNode
490- err error
491- }{promoted: promoted, err: err}
492- return false // Remove idle callback (G_SOURCE_REMOVE)
493- })
494-
495- // Wait for GTK completion
496- result := <-gtkComplete
497-
498- if result.err != nil {
499- log.Printf("[concurrency] Close operation failed on main thread: %v", result.err)
500- return nil, result.err
501- }
502-
503- log.Printf("[concurrency] Close operation completed successfully on main thread")
504- return result.promoted, nil
505-}
506-
507-func (worker *OperationWorker) executeStackOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) {
508- log.Printf("[concurrency] Executing stack operation on node %p", req.TargetNode)
509-
510- // Get the workspace manager from the controller
511- wm := worker.controller.workspaceManager
512- if wm == nil {
513- return nil, fmt.Errorf("workspace manager not available")
514- }
515-
516- // Fast-path when we are already on the GTK main thread.
517- if webkit.IsMainThread() {
518- log.Printf("[concurrency] Already on main thread, executing stack directly")
519- newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode)
520- if err != nil {
521- log.Printf("[concurrency] Stack operation failed: %v", err)
522- return nil, err
523- }
524- log.Printf("[concurrency] Stack operation completed successfully (direct): newNode=%p", newNode)
525- return newNode, nil
526- }
527-
528- // GTK operations must be executed on the main thread
529- // Use IdleAdd to marshal the GTK work to the main thread
530- gtkComplete := make(chan struct {
531- newNode *paneNode
532- err error
533- }, 1)
534-
535- log.Printf("[concurrency] Marshaling stack operation to main thread")
536-
537- // Schedule GTK work on main thread using IdleAdd
538- webkit.IdleAdd(func() bool {
539- // Perform actual GTK stack operations on main thread
540- newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode)
541- gtkComplete <- struct {
542- newNode *paneNode
543- err error
544- }{newNode: newNode, err: err}
545- return false // Remove idle callback (G_SOURCE_REMOVE)
546- })
547-
548- // Wait for GTK completion
549- result := <-gtkComplete
550-
551- if result.err != nil {
552- log.Printf("[concurrency] Stack operation failed on main thread: %v", result.err)
553- return nil, result.err
554- }
555-
556- log.Printf("[concurrency] Stack operation completed successfully on main thread: newNode=%p", result.newNode)
557- return result.newNode, nil
558-}
559-
560-func (worker *OperationWorker) executeFocusOperation(req *OperationRequest, tx *WidgetTransaction) error {
561- // This would integrate with your existing focus logic
562- log.Printf("[concurrency] Executing focus operation on node %p", req.TargetNode)
563- // TODO: Implement actual focus logic with transaction support
564- return fmt.Errorf("focus operation not yet implemented")
565-}
566-
567-func (worker *OperationWorker) executeResizeOperation(req *OperationRequest, tx *WidgetTransaction) error {
568- // This would integrate with your existing resize logic
569- log.Printf("[concurrency] Executing resize operation on node %p", req.TargetNode)
570- // TODO: Implement actual resize logic with transaction support
571- return fmt.Errorf("resize operation not yet implemented")
572-}
573-
574-// sendResult sends the operation result and records stats
575-func (worker *OperationWorker) sendResult(req *OperationRequest, result OperationResult, startTime time.Time, queueTime time.Duration) {
576- result.Duration = time.Since(startTime)
577-
578- // Record stats
579- stats := OperationStats{
580- OperationType: req.Type,
581- Duration: result.Duration,
582- Success: result.Success,
583- RetryCount: req.RetryCount,
584- QueueTime: queueTime,
585- SequenceNum: req.SequenceNum,
586- Timestamp: time.Now(),
587- }
588-
589- worker.controller.recordStats(stats)
590-
591- // Send result
592- select {
593- case req.ResultChan <- result:
594- case <-req.Context.Done():
595- // Context cancelled, result channel may be closed
596- }
597-
598- log.Printf("[concurrency] Worker %d completed operation %s: success=%v, duration=%v",
599- worker.id, req.ID, result.Success, result.Duration)
600-}
601-
602-// recordStats records operation statistics
603-func (cc *ConcurrencyController) recordStats(stats OperationStats) {
604- cc.historyMutex.Lock()
605- defer cc.historyMutex.Unlock()
606-
607- if len(cc.operationHistory) >= cc.maxHistory {
608- cc.operationHistory = cc.operationHistory[1:]
609- }
610- cc.operationHistory = append(cc.operationHistory, stats)
611-}
612-
613-// GetConcurrencyStats returns performance statistics
614-func (cc *ConcurrencyController) GetConcurrencyStats() map[string]interface{} {
615- cc.historyMutex.RLock()
616- defer cc.historyMutex.RUnlock()
617-
618- totalOps := len(cc.operationHistory)
619- if totalOps == 0 {
620- return map[string]interface{}{
621- "total_operations": 0,
622- }
623- }
624-
625- successCount := 0
626- totalDuration := time.Duration(0)
627- totalQueueTime := time.Duration(0)
628- totalRetries := 0
629-
630- opTypeCounts := make(map[OperationType]int)
631-
632- for _, stats := range cc.operationHistory {
633- if stats.Success {
634- successCount++
635- }
636- totalDuration += stats.Duration
637- totalQueueTime += stats.QueueTime
638- totalRetries += stats.RetryCount
639- opTypeCounts[stats.OperationType]++
640- }
641-
642- return map[string]interface{}{
643- "total_operations": totalOps,
644- "successful_operations": successCount,
645- "success_rate": float64(successCount) / float64(totalOps),
646- "avg_duration_ms": (totalDuration / time.Duration(totalOps)).Milliseconds(),
647- "avg_queue_time_ms": (totalQueueTime / time.Duration(totalOps)).Milliseconds(),
648- "total_retries": totalRetries,
649- "operation_counts": opTypeCounts,
650- "queue_size": len(cc.operationQueue),
651- "worker_count": cc.workerCount,
652- "global_sequence": atomic.LoadUint64(&cc.globalSequence),
653- }
654-}
655-
656-// Shutdown stops the concurrency controller
657-func (cc *ConcurrencyController) Shutdown() {
658- cc.shutdownOnce.Do(func() {
659- log.Printf("[concurrency] Shutting down concurrency controller")
660- close(cc.shutdown)
661-
662- // Stop all workers
663- for _, worker := range cc.workers {
664- close(worker.shutdown)
665- }
666-
667- // Wait for workers to finish with timeout
668- done := make(chan struct{})
669- go func() {
670- for _, worker := range cc.workers {
671- worker.wg.Wait()
672- }
673- close(done)
674- }()
675-
676- select {
677- case <-done:
678- log.Printf("[concurrency] All workers stopped gracefully")
679- case <-time.After(5 * time.Second):
680- log.Printf("[concurrency] Shutdown timeout, some workers may not have stopped gracefully")
681- }
682- })
683-}
684-
685-// Helper functions
686-
687-// isAncestor checks if node1 is an ancestor of node2
688-func isAncestor(node1, node2 *paneNode) bool {
689- if node1 == nil || node2 == nil {
690- return false
691- }
692-
693- current := node2.parent
694- for current != nil {
695- if current == node1 {
696- return true
697- }
698- current = current.parent
699- }
700- return false
701-}
@@ -1,701 +0,0 @@
1-// workspace_concurrency.go - Sequence-based concurrency control for bulletproof tree operations
2-package browser
3-
4-import (
5- "context"
6- "fmt"
7- "log"
8- "sync"
9- "sync/atomic"
10- "time"
11-
12- "github.com/bnema/dumber/pkg/webkit"
13-)
14-
15-// OperationType represents different types of tree operations
16-type OperationType int
17-
18-const (
19- OpTypeSplit OperationType = iota
20- OpTypeClose
21- OpTypeStack
22- OpTypeFocus
23- OpTypeResize
24-)
25-
26-func (ot OperationType) String() string {
27- switch ot {
28- case OpTypeSplit:
29- return "split"
30- case OpTypeClose:
31- return "close"
32- case OpTypeStack:
33- return "stack"
34- case OpTypeFocus:
35- return "focus"
36- case OpTypeResize:
37- return "resize"
38- default:
39- return "unknown"
40- }
41-}
42-
43-// OperationRequest represents a pending tree operation
44-type OperationRequest struct {
45- ID string
46- Type OperationType
47- TargetNode *paneNode
48- Direction string // For split operations
49- Parameters map[string]interface{}
50- SequenceNum uint64
51- SubmittedAt time.Time
52- Context context.Context
53- ResultChan chan OperationResult
54- RetryCount int
55- MaxRetries int
56-}
57-
58-// OperationResult represents the result of an operation
59-type OperationResult struct {
60- Success bool
61- Error error
62- NewNode *paneNode
63- Duration time.Duration
64- SequenceNum uint64
65-}
66-
67-// ConcurrencyController manages concurrent access to the tree
68-type ConcurrencyController struct {
69- // Sequence numbers for operation ordering
70- globalSequence uint64
71- nodeSequences map[*paneNode]uint64
72- sequenceMutex sync.RWMutex
73-
74- // Operation queuing and processing
75- operationQueue chan *OperationRequest
76- workerCount int
77- workers []*OperationWorker
78- shutdown chan struct{}
79- shutdownOnce sync.Once
80-
81- // Tree-level synchronization
82- treeMutex sync.RWMutex
83- readOperations map[string]context.CancelFunc // Read operations that can be cancelled
84- writeOperations map[string]*OperationRequest // Active write operations
85-
86- // Performance tracking
87- operationHistory []OperationStats
88- historyMutex sync.RWMutex
89- maxHistory int
90-
91- // Configuration
92- maxQueueSize int
93- operationTimeout time.Duration
94- retryBackoff time.Duration
95- deadlockTimeout time.Duration
96-
97- // Dependencies
98- widgetTxManager *WidgetTransactionManager
99- treeValidator *TreeValidator
100- workspaceManager *WorkspaceManager
101-}
102-
103-// OperationStats tracks performance metrics for operations
104-type OperationStats struct {
105- OperationType OperationType
106- Duration time.Duration
107- Success bool
108- RetryCount int
109- QueueTime time.Duration
110- SequenceNum uint64
111- Timestamp time.Time
112- ConflictCount int
113-}
114-
115-// OperationWorker processes operations from the queue
116-type OperationWorker struct {
117- id int
118- controller *ConcurrencyController
119- shutdown chan struct{}
120- wg *sync.WaitGroup
121-}
122-
123-// NewConcurrencyController creates a new concurrency controller
124-func NewConcurrencyController(workerCount int, widgetTxManager *WidgetTransactionManager, treeValidator *TreeValidator) *ConcurrencyController {
125- if workerCount <= 0 {
126- workerCount = 2 // Default to 2 workers
127- }
128-
129- cc := &ConcurrencyController{
130- globalSequence: 0,
131- nodeSequences: make(map[*paneNode]uint64),
132- operationQueue: make(chan *OperationRequest, 1000), // Buffered channel
133- workerCount: workerCount,
134- workers: make([]*OperationWorker, workerCount),
135- shutdown: make(chan struct{}),
136- readOperations: make(map[string]context.CancelFunc),
137- writeOperations: make(map[string]*OperationRequest),
138- operationHistory: make([]OperationStats, 0, 1000),
139- maxHistory: 1000,
140- maxQueueSize: 1000,
141- operationTimeout: 10 * time.Second,
142- retryBackoff: 100 * time.Millisecond,
143- deadlockTimeout: 30 * time.Second,
144- widgetTxManager: widgetTxManager,
145- treeValidator: treeValidator,
146- }
147-
148- // Start workers
149- var wg sync.WaitGroup
150- for i := 0; i < workerCount; i++ {
151- worker := &OperationWorker{
152- id: i,
153- controller: cc,
154- shutdown: make(chan struct{}),
155- wg: &wg,
156- }
157- cc.workers[i] = worker
158- wg.Add(1)
159- go worker.run()
160- }
161-
162- log.Printf("[concurrency] Started concurrency controller with %d workers", workerCount)
163- return cc
164-}
165-
166-// SetWorkspaceManager sets the workspace manager reference (called after construction to avoid circular dependency)
167-func (cc *ConcurrencyController) SetWorkspaceManager(wm *WorkspaceManager) {
168- cc.workspaceManager = wm
169-}
170-
171-// SubmitOperation submits an operation for execution
172-func (cc *ConcurrencyController) SubmitOperation(req *OperationRequest) <-chan OperationResult {
173- // Assign sequence number
174- req.SequenceNum = atomic.AddUint64(&cc.globalSequence, 1)
175- req.SubmittedAt = time.Now()
176- req.ResultChan = make(chan OperationResult, 1)
177-
178- // Set default retry count if not specified
179- if req.MaxRetries == 0 {
180- req.MaxRetries = 3
181- }
182-
183- // Set default context if not provided
184- if req.Context == nil {
185- ctx, cancel := context.WithTimeout(context.Background(), cc.operationTimeout)
186- req.Context = ctx
187- // Schedule cleanup
188- go func() {
189- <-ctx.Done()
190- cancel()
191- }()
192- }
193-
194- // Try to queue the operation
195- select {
196- case cc.operationQueue <- req:
197- log.Printf("[concurrency] Queued operation %s (seq=%d, type=%s)", req.ID, req.SequenceNum, req.Type)
198- case <-req.Context.Done():
199- // Context cancelled before queuing
200- req.ResultChan <- OperationResult{
201- Success: false,
202- Error: fmt.Errorf("operation cancelled before queuing: %w", req.Context.Err()),
203- }
204- default:
205- // Queue is full
206- req.ResultChan <- OperationResult{
207- Success: false,
208- Error: fmt.Errorf("operation queue is full"),
209- }
210- }
211-
212- return req.ResultChan
213-}
214-
215-// worker run loop
216-func (worker *OperationWorker) run() {
217- defer worker.wg.Done()
218- log.Printf("[concurrency] Worker %d started", worker.id)
219-
220- for {
221- select {
222- case req := <-worker.controller.operationQueue:
223- worker.processOperation(req)
224- case <-worker.shutdown:
225- log.Printf("[concurrency] Worker %d shutting down", worker.id)
226- return
227- case <-worker.controller.shutdown:
228- log.Printf("[concurrency] Worker %d shutting down (global)", worker.id)
229- return
230- }
231- }
232-}
233-
234-// processOperation processes a single operation request
235-func (worker *OperationWorker) processOperation(req *OperationRequest) {
236- startTime := time.Now()
237- queueTime := startTime.Sub(req.SubmittedAt)
238-
239- log.Printf("[concurrency] Worker %d processing operation %s (seq=%d, queue_time=%v)",
240- worker.id, req.ID, req.SequenceNum, queueTime)
241-
242- result := OperationResult{
243- SequenceNum: req.SequenceNum,
244- }
245-
246- // Check if context is already cancelled
247- select {
248- case <-req.Context.Done():
249- result.Success = false
250- result.Error = fmt.Errorf("operation context cancelled: %w", req.Context.Err())
251- worker.sendResult(req, result, startTime, queueTime)
252- return
253- default:
254- }
255-
256- // Check for conflicts and acquire locks
257- conflicts := worker.checkConflicts(req)
258- if conflicts > 0 && req.RetryCount < req.MaxRetries {
259- // Retry with backoff
260- req.RetryCount++
261- backoff := time.Duration(req.RetryCount) * worker.controller.retryBackoff
262-
263- log.Printf("[concurrency] Operation %s has %d conflicts, retrying in %v (attempt %d/%d)",
264- req.ID, conflicts, backoff, req.RetryCount, req.MaxRetries)
265-
266- go func() {
267- time.Sleep(backoff)
268- select {
269- case worker.controller.operationQueue <- req:
270- case <-req.Context.Done():
271- result.Success = false
272- result.Error = fmt.Errorf("operation cancelled during retry")
273- req.ResultChan <- result
274- }
275- }()
276- return
277- }
278-
279- if conflicts > 0 {
280- result.Success = false
281- result.Error = fmt.Errorf("operation failed after %d retries due to conflicts", req.MaxRetries)
282- worker.sendResult(req, result, startTime, queueTime)
283- return
284- }
285-
286- // Execute the operation
287- err := worker.executeOperation(req, &result)
288- if err != nil {
289- result.Success = false
290- result.Error = err
291- } else {
292- result.Success = true
293- }
294-
295- // Record stats and send result
296- worker.sendResult(req, result, startTime, queueTime)
297-}
298-
299-// checkConflicts checks for operation conflicts
300-func (worker *OperationWorker) checkConflicts(req *OperationRequest) int {
301- cc := worker.controller
302- conflicts := 0
303-
304- cc.treeMutex.RLock()
305- defer cc.treeMutex.RUnlock()
306-
307- // Check for conflicting write operations on the same node
308- for _, activeOp := range cc.writeOperations {
309- if activeOp.TargetNode == req.TargetNode {
310- conflicts++
311- }
312- // Check for parent-child conflicts
313- if req.TargetNode != nil && activeOp.TargetNode != nil {
314- if isAncestor(activeOp.TargetNode, req.TargetNode) || isAncestor(req.TargetNode, activeOp.TargetNode) {
315- conflicts++
316- }
317- }
318- }
319-
320- return conflicts
321-}
322-
323-// executeOperation executes the actual operation
324-func (worker *OperationWorker) executeOperation(req *OperationRequest, result *OperationResult) error {
325- cc := worker.controller
326-
327- // Acquire tree write lock
328- cc.treeMutex.Lock()
329- defer cc.treeMutex.Unlock()
330-
331- // Register this operation as active
332- cc.writeOperations[req.ID] = req
333- defer delete(cc.writeOperations, req.ID)
334-
335- // Update node sequence
336- if req.TargetNode != nil {
337- cc.sequenceMutex.Lock()
338- cc.nodeSequences[req.TargetNode] = req.SequenceNum
339- cc.sequenceMutex.Unlock()
340- }
341-
342- // Create widget transaction for this operation
343- txID := fmt.Sprintf("op_%s_%d", req.ID, req.SequenceNum)
344- tx := cc.widgetTxManager.BeginTransaction(txID)
345-
346- // Execute based on operation type
347- var err error
348- switch req.Type {
349- case OpTypeSplit:
350- result.NewNode, err = worker.executeSplitOperation(req, tx)
351- case OpTypeClose:
352- result.NewNode, err = worker.executeCloseOperation(req, tx)
353- case OpTypeStack:
354- result.NewNode, err = worker.executeStackOperation(req, tx)
355- case OpTypeFocus:
356- err = worker.executeFocusOperation(req, tx)
357- case OpTypeResize:
358- err = worker.executeResizeOperation(req, tx)
359- default:
360- err = fmt.Errorf("unknown operation type: %s", req.Type)
361- }
362-
363- if err != nil {
364- // Rollback transaction on error
365- if rollbackErr := tx.Rollback(); rollbackErr != nil {
366- log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr)
367- }
368- cc.widgetTxManager.FinishTransaction(txID, false, err.Error())
369- return err
370- }
371-
372- // Execute widget transaction
373- if err = tx.Execute(); err != nil {
374- if rollbackErr := tx.Rollback(); rollbackErr != nil {
375- log.Printf("[concurrency] Failed to rollback transaction %s: %v", txID, rollbackErr)
376- }
377- cc.widgetTxManager.FinishTransaction(txID, false, err.Error())
378- return fmt.Errorf("widget transaction failed: %w", err)
379- }
380-
381- // Commit transaction
382- if err = tx.Commit(); err != nil {
383- cc.widgetTxManager.FinishTransaction(txID, false, err.Error())
384- return fmt.Errorf("failed to commit transaction: %w", err)
385- }
386-
387- cc.widgetTxManager.FinishTransaction(txID, true, "")
388-
389- // Validate tree if validator is available
390- if cc.treeValidator != nil {
391- if validateErr := cc.treeValidator.ValidateTree(req.TargetNode, req.Type.String()); validateErr != nil {
392- log.Printf("[concurrency] Tree validation failed after operation %s: %v", req.ID, validateErr)
393- // Don't return error for validation failures in production, just log
394- }
395- }
396-
397- return nil
398-}
399-
400-// Placeholder implementations for operation execution
401-func (worker *OperationWorker) executeSplitOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) {
402- log.Printf("[concurrency] Executing split operation on node %p direction=%s", req.TargetNode, req.Direction)
403-
404- // Get the workspace manager from the controller
405- wm := worker.controller.workspaceManager
406- if wm == nil {
407- return nil, fmt.Errorf("workspace manager not available")
408- }
409-
410- // Fast-path when we are already on the GTK main thread.
411- if webkit.IsMainThread() {
412- log.Printf("[concurrency] Already on main thread, executing split directly")
413- newNode, err := wm.splitNode(req.TargetNode, req.Direction)
414- if err != nil {
415- log.Printf("[concurrency] Split operation failed: %v", err)
416- return nil, err
417- }
418- log.Printf("[concurrency] Split operation completed successfully (direct): newNode=%p", newNode)
419- return newNode, nil
420- }
421-
422- // GTK operations must be executed on the main thread
423- // Use IdleAdd to marshal the GTK work to the main thread
424- gtkComplete := make(chan struct {
425- newNode *paneNode
426- err error
427- }, 1)
428-
429- log.Printf("[concurrency] Marshaling split operation to main thread")
430-
431- // Schedule GTK work on main thread using IdleAdd
432- webkit.IdleAdd(func() bool {
433- // Perform actual GTK split operations on main thread
434- newNode, err := wm.splitNode(req.TargetNode, req.Direction)
435- gtkComplete <- struct {
436- newNode *paneNode
437- err error
438- }{newNode: newNode, err: err}
439- return false // Remove idle callback (G_SOURCE_REMOVE)
440- })
441-
442- // Wait for GTK completion
443- result := <-gtkComplete
444-
445- if result.err != nil {
446- log.Printf("[concurrency] Split operation failed on main thread: %v", result.err)
447- return nil, result.err
448- }
449-
450- log.Printf("[concurrency] Split operation completed successfully on main thread: newNode=%p", result.newNode)
451- return result.newNode, nil
452-}
453-
454-func (worker *OperationWorker) executeCloseOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) {
455- log.Printf("[concurrency] Executing close operation on node %p", req.TargetNode)
456-
457- // Get the workspace manager from the controller
458- wm := worker.controller.workspaceManager
459- if wm == nil {
460- return nil, fmt.Errorf("workspace manager not available")
461- }
462-
463- // Fast-path when we are already on the GTK main thread.
464- if webkit.IsMainThread() {
465- log.Printf("[concurrency] Already on main thread, executing close directly")
466- promoted, err := wm.closePane(req.TargetNode)
467- if err != nil {
468- log.Printf("[concurrency] Close operation failed: %v", err)
469- return nil, err
470- }
471- log.Printf("[concurrency] Close operation completed successfully (direct)")
472- return promoted, nil
473- }
474-
475- // GTK operations must be executed on the main thread
476- // Use IdleAdd to marshal the GTK work to the main thread
477- gtkComplete := make(chan struct {
478- promoted *paneNode
479- err error
480- }, 1)
481-
482- log.Printf("[concurrency] Marshaling close operation to main thread")
483-
484- // Schedule GTK work on main thread using IdleAdd
485- webkit.IdleAdd(func() bool {
486- // Perform actual GTK close operations on main thread
487- promoted, err := wm.closePane(req.TargetNode)
488- gtkComplete <- struct {
489- promoted *paneNode
490- err error
491- }{promoted: promoted, err: err}
492- return false // Remove idle callback (G_SOURCE_REMOVE)
493- })
494-
495- // Wait for GTK completion
496- result := <-gtkComplete
497-
498- if result.err != nil {
499- log.Printf("[concurrency] Close operation failed on main thread: %v", result.err)
500- return nil, result.err
501- }
502-
503- log.Printf("[concurrency] Close operation completed successfully on main thread")
504- return result.promoted, nil
505-}
506-
507-func (worker *OperationWorker) executeStackOperation(req *OperationRequest, tx *WidgetTransaction) (*paneNode, error) {
508- log.Printf("[concurrency] Executing stack operation on node %p", req.TargetNode)
509-
510- // Get the workspace manager from the controller
511- wm := worker.controller.workspaceManager
512- if wm == nil {
513- return nil, fmt.Errorf("workspace manager not available")
514- }
515-
516- // Fast-path when we are already on the GTK main thread.
517- if webkit.IsMainThread() {
518- log.Printf("[concurrency] Already on main thread, executing stack directly")
519- newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode)
520- if err != nil {
521- log.Printf("[concurrency] Stack operation failed: %v", err)
522- return nil, err
523- }
524- log.Printf("[concurrency] Stack operation completed successfully (direct): newNode=%p", newNode)
525- return newNode, nil
526- }
527-
528- // GTK operations must be executed on the main thread
529- // Use IdleAdd to marshal the GTK work to the main thread
530- gtkComplete := make(chan struct {
531- newNode *paneNode
532- err error
533- }, 1)
534-
535- log.Printf("[concurrency] Marshaling stack operation to main thread")
536-
537- // Schedule GTK work on main thread using IdleAdd
538- webkit.IdleAdd(func() bool {
539- // Perform actual GTK stack operations on main thread
540- newNode, err := wm.stackedPaneManager.StackPane(req.TargetNode)
541- gtkComplete <- struct {
542- newNode *paneNode
543- err error
544- }{newNode: newNode, err: err}
545- return false // Remove idle callback (G_SOURCE_REMOVE)
546- })
547-
548- // Wait for GTK completion
549- result := <-gtkComplete
550-
551- if result.err != nil {
552- log.Printf("[concurrency] Stack operation failed on main thread: %v", result.err)
553- return nil, result.err
554- }
555-
556- log.Printf("[concurrency] Stack operation completed successfully on main thread: newNode=%p", result.newNode)
557- return result.newNode, nil
558-}
559-
560-func (worker *OperationWorker) executeFocusOperation(req *OperationRequest, tx *WidgetTransaction) error {
561- // This would integrate with your existing focus logic
562- log.Printf("[concurrency] Executing focus operation on node %p", req.TargetNode)
563- // TODO: Implement actual focus logic with transaction support
564- return fmt.Errorf("focus operation not yet implemented")
565-}
566-
567-func (worker *OperationWorker) executeResizeOperation(req *OperationRequest, tx *WidgetTransaction) error {
568- // This would integrate with your existing resize logic
569- log.Printf("[concurrency] Executing resize operation on node %p", req.TargetNode)
570- // TODO: Implement actual resize logic with transaction support
571- return fmt.Errorf("resize operation not yet implemented")
572-}
573-
574-// sendResult sends the operation result and records stats
575-func (worker *OperationWorker) sendResult(req *OperationRequest, result OperationResult, startTime time.Time, queueTime time.Duration) {
576- result.Duration = time.Since(startTime)
577-
578- // Record stats
579- stats := OperationStats{
580- OperationType: req.Type,
581- Duration: result.Duration,
582- Success: result.Success,
583- RetryCount: req.RetryCount,
584- QueueTime: queueTime,
585- SequenceNum: req.SequenceNum,
586- Timestamp: time.Now(),
587- }
588-
589- worker.controller.recordStats(stats)
590-
591- // Send result
592- select {
593- case req.ResultChan <- result:
594- case <-req.Context.Done():
595- // Context cancelled, result channel may be closed
596- }
597-
598- log.Printf("[concurrency] Worker %d completed operation %s: success=%v, duration=%v",
599- worker.id, req.ID, result.Success, result.Duration)
600-}
601-
602-// recordStats records operation statistics
603-func (cc *ConcurrencyController) recordStats(stats OperationStats) {
604- cc.historyMutex.Lock()
605- defer cc.historyMutex.Unlock()
606-
607- if len(cc.operationHistory) >= cc.maxHistory {
608- cc.operationHistory = cc.operationHistory[1:]
609- }
610- cc.operationHistory = append(cc.operationHistory, stats)
611-}
612-
613-// GetConcurrencyStats returns performance statistics
614-func (cc *ConcurrencyController) GetConcurrencyStats() map[string]interface{} {
615- cc.historyMutex.RLock()
616- defer cc.historyMutex.RUnlock()
617-
618- totalOps := len(cc.operationHistory)
619- if totalOps == 0 {
620- return map[string]interface{}{
621- "total_operations": 0,
622- }
623- }
624-
625- successCount := 0
626- totalDuration := time.Duration(0)
627- totalQueueTime := time.Duration(0)
628- totalRetries := 0
629-
630- opTypeCounts := make(map[OperationType]int)
631-
632- for _, stats := range cc.operationHistory {
633- if stats.Success {
634- successCount++
635- }
636- totalDuration += stats.Duration
637- totalQueueTime += stats.QueueTime
638- totalRetries += stats.RetryCount
639- opTypeCounts[stats.OperationType]++
640- }
641-
642- return map[string]interface{}{
643- "total_operations": totalOps,
644- "successful_operations": successCount,
645- "success_rate": float64(successCount) / float64(totalOps),
646- "avg_duration_ms": (totalDuration / time.Duration(totalOps)).Milliseconds(),
647- "avg_queue_time_ms": (totalQueueTime / time.Duration(totalOps)).Milliseconds(),
648- "total_retries": totalRetries,
649- "operation_counts": opTypeCounts,
650- "queue_size": len(cc.operationQueue),
651- "worker_count": cc.workerCount,
652- "global_sequence": atomic.LoadUint64(&cc.globalSequence),
653- }
654-}
655-
656-// Shutdown stops the concurrency controller
657-func (cc *ConcurrencyController) Shutdown() {
658- cc.shutdownOnce.Do(func() {
659- log.Printf("[concurrency] Shutting down concurrency controller")
660- close(cc.shutdown)
661-
662- // Stop all workers
663- for _, worker := range cc.workers {
664- close(worker.shutdown)
665- }
666-
667- // Wait for workers to finish with timeout
668- done := make(chan struct{})
669- go func() {
670- for _, worker := range cc.workers {
671- worker.wg.Wait()
672- }
673- close(done)
674- }()
675-
676- select {
677- case <-done:
678- log.Printf("[concurrency] All workers stopped gracefully")
679- case <-time.After(5 * time.Second):
680- log.Printf("[concurrency] Shutdown timeout, some workers may not have stopped gracefully")
681- }
682- })
683-}
684-
685-// Helper functions
686-
687-// isAncestor checks if node1 is an ancestor of node2
688-func isAncestor(node1, node2 *paneNode) bool {
689- if node1 == nil || node2 == nil {
690- return false
691- }
692-
693- current := node2.parent
694- for current != nil {
695- if current == node1 {
696- return true
697- }
698- current = current.parent
699- }
700- return false
701-}
M internal/app/browser/workspace_manager.go +2 -7
@@ -57,10 +57,9 @@ type WorkspaceManager struct {
5757 lastFocusTarget *paneNode
5858 focusDebounce time.Duration
5959
60- // BULLETPROOF COMPONENTS: Enhanced validation and safety systems
60+ // ENHANCED COMPONENTS: Validation and safety systems (opt-in for debugging)
6161 treeValidator *TreeValidator
6262 widgetTxManager *WidgetTransactionManager
63- concurrencyController *ConcurrencyController
6463 treeRebalancer *TreeRebalancer
6564 geometryValidator *GeometryValidator
6665 stackLifecycleManager *StackLifecycleManager
@@ -95,14 +94,10 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
9594 manager.geometryValidator = NewGeometryValidator()
9695 manager.stateTombstoneManager = NewStateTombstoneManager(manager)
9796
98- // Initialize concurrency controller with bulletproof components
99- manager.concurrencyController = NewConcurrencyController(2, manager.widgetTxManager, manager.treeValidator)
100- manager.concurrencyController.SetWorkspaceManager(manager)
101-
10297 // Initialize tree rebalancer
10398 manager.treeRebalancer = NewTreeRebalancer(manager, manager.treeValidator, manager.widgetTxManager)
10499
105- // Initialize stack lifecycle manager with bulletproof components
100+ // Initialize stack lifecycle manager with enhanced components
106101 manager.stackLifecycleManager = NewStackLifecycleManager(manager, manager.treeValidator, manager.widgetTxManager)
107102
108103 // Initialize existing specialized managers (now enhanced with bulletproof components)
@@ -57,10 +57,9 @@ type WorkspaceManager struct {
57 lastFocusTarget *paneNode57 lastFocusTarget *paneNode
58 focusDebounce time.Duration58 focusDebounce time.Duration
59 59
60- // BULLETPROOF COMPONENTS: Enhanced validation and safety systems60+ // ENHANCED COMPONENTS: Validation and safety systems (opt-in for debugging)
61 treeValidator *TreeValidator61 treeValidator *TreeValidator
62 widgetTxManager *WidgetTransactionManager62 widgetTxManager *WidgetTransactionManager
63- concurrencyController *ConcurrencyController
64 treeRebalancer *TreeRebalancer63 treeRebalancer *TreeRebalancer
65 geometryValidator *GeometryValidator64 geometryValidator *GeometryValidator
66 stackLifecycleManager *StackLifecycleManager65 stackLifecycleManager *StackLifecycleManager
@@ -95,14 +94,10 @@ func NewWorkspaceManager(app *BrowserApp, rootPane *BrowserPane) *WorkspaceManag
95 manager.geometryValidator = NewGeometryValidator()94 manager.geometryValidator = NewGeometryValidator()
96 manager.stateTombstoneManager = NewStateTombstoneManager(manager)95 manager.stateTombstoneManager = NewStateTombstoneManager(manager)
97 96
98- // Initialize concurrency controller with bulletproof components
99- manager.concurrencyController = NewConcurrencyController(2, manager.widgetTxManager, manager.treeValidator)
100- manager.concurrencyController.SetWorkspaceManager(manager)
101-
102 // Initialize tree rebalancer97 // Initialize tree rebalancer
103 manager.treeRebalancer = NewTreeRebalancer(manager, manager.treeValidator, manager.widgetTxManager)98 manager.treeRebalancer = NewTreeRebalancer(manager, manager.treeValidator, manager.widgetTxManager)
104 99
105- // Initialize stack lifecycle manager with bulletproof components100+ // Initialize stack lifecycle manager with enhanced components
106 manager.stackLifecycleManager = NewStackLifecycleManager(manager, manager.treeValidator, manager.widgetTxManager)101 manager.stackLifecycleManager = NewStackLifecycleManager(manager, manager.treeValidator, manager.widgetTxManager)
107 102
108 // Initialize existing specialized managers (now enhanced with bulletproof components)103 // Initialize existing specialized managers (now enhanced with bulletproof components)
M internal/app/browser/workspace_operations.go +82 -143
@@ -1,11 +1,9 @@
1-// workspace_bulletproof_operations.go - Bulletproof wrapper methods for all tree operations
1+// workspace_operations.go - Workspace operation methods with validation and safety
22 package browser
33
44 import (
5- "context"
65 "fmt"
76 "log"
8- "time"
97
108 "github.com/bnema/dumber/pkg/webkit"
119 )
@@ -64,58 +62,37 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
6462 return newNode, nil
6563 }
6664
67- // Step 5: Not on main thread, marshal through concurrency controller
68- opReq := &OperationRequest{
69- ID: fmt.Sprintf("split_%p_%d", target, time.Now().UnixNano()),
70- Type: OpTypeSplit,
71- TargetNode: target,
72- Direction: direction,
73- Parameters: map[string]interface{}{
74- "direction": direction,
75- },
76- Context: context.Background(),
77- MaxRetries: 3,
78- }
79-
80- resultChan := wm.concurrencyController.SubmitOperation(opReq)
81-
82- log.Printf("[workspace] Waiting for operation result while pumping GTK events")
83- ticker := time.NewTicker(10 * time.Millisecond)
84- defer ticker.Stop()
85-
86- timeout := time.NewTimer(30 * time.Second)
87- defer timeout.Stop()
88-
89- for {
90- select {
91- case result := <-resultChan:
92- if !result.Success {
93- if tombstone != nil {
94- if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
95- log.Printf("[bulletproof] Rollback failed after split failure: %v", rollbackErr)
96- }
65+ // Step 5: Not on main thread, marshal via IdleAdd
66+ log.Printf("[workspace] Not on main thread, marshalling split via IdleAdd")
67+ var newNode *paneNode
68+ var splitErr error
69+ done := make(chan struct{})
70+
71+ webkit.IdleAdd(func() bool {
72+ newNode, splitErr = wm.splitNode(target, direction)
73+ if splitErr != nil {
74+ if tombstone != nil {
75+ if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
76+ log.Printf("[workspace] Rollback failed after split failure: %v", rollbackErr)
9777 }
98- return nil, result.Error
9978 }
100-
101- if err := wm.treeValidator.ValidateTree(wm.root, "after_split"); err != nil {
102- log.Printf("[workspace] Tree validation failed after split: %v", err)
79+ } else {
80+ if verr := wm.treeValidator.ValidateTree(wm.root, "after_split"); verr != nil {
81+ log.Printf("[workspace] Tree validation failed after split: %v", verr)
10382 }
83+ }
84+ close(done)
85+ return false
86+ })
10487
105- // Tree rebalancing is only needed after close promotions. Splits have correct allocation from GTK.
106-
107- log.Printf("[workspace] Split operation completed successfully: newNode=%p", result.NewNode)
108- return result.NewNode, nil
88+ <-done
10989
110- case <-ticker.C:
111- if webkit.IsMainThread() {
112- webkit.IterateMainLoop()
113- }
114-
115- case <-timeout.C:
116- return nil, fmt.Errorf("split operation timed out")
117- }
90+ if splitErr != nil {
91+ return nil, splitErr
11892 }
93+
94+ log.Printf("[workspace] Split operation completed successfully: newNode=%p", newNode)
95+ return newNode, nil
11996 }
12097
12198 // ClosePane performs a close operation with validation and safety checks
@@ -178,64 +155,48 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
178155 return nil
179156 }
180157
181- // Step 5: Not on main thread, marshal through concurrency controller
182- log.Printf("[workspace] Not on main thread, using concurrency controller")
183- opReq := &OperationRequest{
184- ID: fmt.Sprintf("close_%p_%d", node, time.Now().UnixNano()),
185- Type: OpTypeClose,
186- TargetNode: node,
187- Parameters: map[string]interface{}{},
188- Context: context.Background(),
189- MaxRetries: 3,
190- }
191-
192- resultChan := wm.concurrencyController.SubmitOperation(opReq)
193-
194- log.Printf("[workspace] Waiting for operation result while pumping GTK events")
195- ticker := time.NewTicker(10 * time.Millisecond)
196- defer ticker.Stop()
197-
198- timeout := time.NewTimer(30 * time.Second)
199- defer timeout.Stop()
200-
201- for {
202- select {
203- case result := <-resultChan:
204- if !result.Success {
205- wm.paneCloseLogf("async close failed node=%p err=%v", node, result.Error)
206- wm.dumpTreeState("after_close_error")
207- if tombstone != nil {
208- if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
209- log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr)
210- }
158+ // Step 5: Not on main thread, marshal via IdleAdd
159+ log.Printf("[workspace] Not on main thread, marshalling close via IdleAdd")
160+ var promoted *paneNode
161+ var closeErr error
162+ done := make(chan struct{})
163+
164+ webkit.IdleAdd(func() bool {
165+ wm.paneCloseLogf("invoking closePane node=%p", node)
166+ promoted, closeErr = wm.closePane(node)
167+ if closeErr != nil {
168+ wm.paneCloseLogf("closePane failed node=%p err=%v", node, closeErr)
169+ wm.dumpTreeState("after_close_error")
170+ if tombstone != nil {
171+ if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
172+ log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr)
211173 }
212- return result.Error
213174 }
214-
215- if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {
216- log.Printf("[workspace] Tree validation failed after close: %v", err)
175+ } else {
176+ if verr := wm.treeValidator.ValidateTree(wm.root, "after_close"); verr != nil {
177+ log.Printf("[workspace] Tree validation failed after close: %v", verr)
217178 }
218- wm.paneCloseLogf("async close succeeded node=%p promoted=%p root=%p", node, result.NewNode, wm.root)
179+ wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root)
219180 wm.dumpTreeState("after_close_success")
220181
221182 if wm.treeRebalancer != nil {
222- if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil {
223- log.Printf("[workspace] Tree rebalancing failed after close: %v", err)
183+ if rerr := wm.treeRebalancer.RebalanceAfterClose(node, promoted); rerr != nil {
184+ log.Printf("[workspace] Tree rebalancing failed after close: %v", rerr)
224185 }
225186 }
187+ }
188+ close(done)
189+ return false
190+ })
226191
227- log.Printf("[workspace] Close operation completed successfully")
228- return nil
229-
230- case <-ticker.C:
231- if webkit.IsMainThread() {
232- webkit.IterateMainLoop()
233- }
192+ <-done
234193
235- case <-timeout.C:
236- return fmt.Errorf("close operation timed out")
237- }
194+ if closeErr != nil {
195+ return closeErr
238196 }
197+
198+ log.Printf("[workspace] Close operation completed successfully")
199+ return nil
239200 }
240201
241202 // StackPane performs a stack operation with validation and safety checks
@@ -275,7 +236,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
275236 if err != nil {
276237 if tombstone != nil {
277238 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
278- log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr)
239+ log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr)
279240 }
280241 }
281242 return nil, err
@@ -289,53 +250,37 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
289250 return newNode, nil
290251 }
291252
292- // Step 5: Not on main thread, marshal through concurrency controller
293- opReq := &OperationRequest{
294- ID: fmt.Sprintf("stack_%p_%d", target, time.Now().UnixNano()),
295- Type: OpTypeStack,
296- TargetNode: target,
297- Parameters: map[string]interface{}{},
298- Context: context.Background(),
299- MaxRetries: 3,
300- }
301-
302- resultChan := wm.concurrencyController.SubmitOperation(opReq)
303-
304- log.Printf("[workspace] Waiting for operation result while pumping GTK events")
305- ticker := time.NewTicker(10 * time.Millisecond)
306- defer ticker.Stop()
253+ // Step 5: Not on main thread, marshal via IdleAdd
254+ log.Printf("[workspace] Not on main thread, marshalling stack via IdleAdd")
255+ var newNode *paneNode
256+ var stackErr error
257+ done := make(chan struct{})
307258
308- timeout := time.NewTimer(30 * time.Second)
309- defer timeout.Stop()
310-
311- for {
312- select {
313- case result := <-resultChan:
314- if !result.Success {
315- if tombstone != nil {
316- if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
317- log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr)
318- }
259+ webkit.IdleAdd(func() bool {
260+ newNode, stackErr = wm.stackedPaneManager.StackPane(target)
261+ if stackErr != nil {
262+ if tombstone != nil {
263+ if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
264+ log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr)
319265 }
320- return nil, result.Error
321266 }
322-
323- if err := wm.treeValidator.ValidateTree(wm.root, "after_stack"); err != nil {
324- log.Printf("[workspace] Tree validation failed after stack: %v", err)
267+ } else {
268+ if verr := wm.treeValidator.ValidateTree(wm.root, "after_stack"); verr != nil {
269+ log.Printf("[workspace] Tree validation failed after stack: %v", verr)
325270 }
271+ }
272+ close(done)
273+ return false
274+ })
326275
327- log.Printf("[workspace] Stack operation completed successfully: newNode=%p", result.NewNode)
328- return result.NewNode, nil
329-
330- case <-ticker.C:
331- if webkit.IsMainThread() {
332- webkit.IterateMainLoop()
333- }
276+ <-done
334277
335- case <-timeout.C:
336- return nil, fmt.Errorf("stack operation timed out")
337- }
278+ if stackErr != nil {
279+ return nil, stackErr
338280 }
281+
282+ log.Printf("[workspace] Stack operation completed successfully: newNode=%p", newNode)
283+ return newNode, nil
339284 }
340285
341286 // EnableEnhancedMode enables all enhanced validation features
@@ -383,9 +328,6 @@ func (wm *WorkspaceManager) GetEnhancedStats() map[string]interface{} {
383328 stats["widget_transactions"] = wm.widgetTxManager.GetTransactionStats()
384329 }
385330
386- if wm.concurrencyController != nil {
387- stats["concurrency"] = wm.concurrencyController.GetConcurrencyStats()
388- }
389331
390332 if wm.treeRebalancer != nil {
391333 stats["tree_rebalancing"] = wm.treeRebalancer.GetRebalancingStats()
@@ -432,9 +374,6 @@ func (wm *WorkspaceManager) ValidateWorkspaceIntegrity() error {
432374 func (wm *WorkspaceManager) ShutdownEnhancedComponents() {
433375 log.Printf("[workspace] Shutting down enhanced validation components")
434376
435- if wm.concurrencyController != nil {
436- wm.concurrencyController.Shutdown()
437- }
438377
439378 // Other components don't require explicit shutdown currently
440379 log.Printf("[workspace] Enhanced validation components shutdown complete")
@@ -1,11 +1,9 @@
1-// workspace_bulletproof_operations.go - Bulletproof wrapper methods for all tree operations1+// workspace_operations.go - Workspace operation methods with validation and safety
2 package browser2 package browser
3 3
4 import (4 import (
5- "context"
6 "fmt"5 "fmt"
7 "log"6 "log"
8- "time"
9 7
10 "github.com/bnema/dumber/pkg/webkit"8 "github.com/bnema/dumber/pkg/webkit"
11 )9 )
@@ -64,58 +62,37 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
64 return newNode, nil62 return newNode, nil
65 }63 }
66 64
67- // Step 5: Not on main thread, marshal through concurrency controller65+ // Step 5: Not on main thread, marshal via IdleAdd
68- opReq := &OperationRequest{66+ log.Printf("[workspace] Not on main thread, marshalling split via IdleAdd")
69- ID: fmt.Sprintf("split_%p_%d", target, time.Now().UnixNano()),67+ var newNode *paneNode
70- Type: OpTypeSplit,68+ var splitErr error
71- TargetNode: target,69+ done := make(chan struct{})
72- Direction: direction,70+
73- Parameters: map[string]interface{}{71+ webkit.IdleAdd(func() bool {
74- "direction": direction,72+ newNode, splitErr = wm.splitNode(target, direction)
75- },73+ if splitErr != nil {
76- Context: context.Background(),74+ if tombstone != nil {
77- MaxRetries: 3,75+ if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
78- }76+ log.Printf("[workspace] Rollback failed after split failure: %v", rollbackErr)
79-
80- resultChan := wm.concurrencyController.SubmitOperation(opReq)
81-
82- log.Printf("[workspace] Waiting for operation result while pumping GTK events")
83- ticker := time.NewTicker(10 * time.Millisecond)
84- defer ticker.Stop()
85-
86- timeout := time.NewTimer(30 * time.Second)
87- defer timeout.Stop()
88-
89- for {
90- select {
91- case result := <-resultChan:
92- if !result.Success {
93- if tombstone != nil {
94- if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
95- log.Printf("[bulletproof] Rollback failed after split failure: %v", rollbackErr)
96- }
97 }77 }
98- return nil, result.Error
99 }78 }
100-79+ } else {
101- if err := wm.treeValidator.ValidateTree(wm.root, "after_split"); err != nil {80+ if verr := wm.treeValidator.ValidateTree(wm.root, "after_split"); verr != nil {
102- log.Printf("[workspace] Tree validation failed after split: %v", err)81+ log.Printf("[workspace] Tree validation failed after split: %v", verr)
103 }82 }
83+ }
84+ close(done)
85+ return false
86+ })
104 87
105- // Tree rebalancing is only needed after close promotions. Splits have correct allocation from GTK.88+ <-done
106-
107- log.Printf("[workspace] Split operation completed successfully: newNode=%p", result.NewNode)
108- return result.NewNode, nil
109 89
110- case <-ticker.C:90+ if splitErr != nil {
111- if webkit.IsMainThread() {91+ return nil, splitErr
112- webkit.IterateMainLoop()
113- }
114-
115- case <-timeout.C:
116- return nil, fmt.Errorf("split operation timed out")
117- }
118 }92 }
93+
94+ log.Printf("[workspace] Split operation completed successfully: newNode=%p", newNode)
95+ return newNode, nil
119 }96 }
120 97
121 // ClosePane performs a close operation with validation and safety checks98 // ClosePane performs a close operation with validation and safety checks
@@ -178,64 +155,48 @@ func (wm *WorkspaceManager) ClosePane(node *paneNode) error {
178 return nil155 return nil
179 }156 }
180 157
181- // Step 5: Not on main thread, marshal through concurrency controller158+ // Step 5: Not on main thread, marshal via IdleAdd
182- log.Printf("[workspace] Not on main thread, using concurrency controller")159+ log.Printf("[workspace] Not on main thread, marshalling close via IdleAdd")
183- opReq := &OperationRequest{160+ var promoted *paneNode
184- ID: fmt.Sprintf("close_%p_%d", node, time.Now().UnixNano()),161+ var closeErr error
185- Type: OpTypeClose,162+ done := make(chan struct{})
186- TargetNode: node,163+
187- Parameters: map[string]interface{}{},164+ webkit.IdleAdd(func() bool {
188- Context: context.Background(),165+ wm.paneCloseLogf("invoking closePane node=%p", node)
189- MaxRetries: 3,166+ promoted, closeErr = wm.closePane(node)
190- }167+ if closeErr != nil {
191-168+ wm.paneCloseLogf("closePane failed node=%p err=%v", node, closeErr)
192- resultChan := wm.concurrencyController.SubmitOperation(opReq)169+ wm.dumpTreeState("after_close_error")
193-170+ if tombstone != nil {
194- log.Printf("[workspace] Waiting for operation result while pumping GTK events")171+ if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
195- ticker := time.NewTicker(10 * time.Millisecond)172+ log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr)
196- defer ticker.Stop()
197-
198- timeout := time.NewTimer(30 * time.Second)
199- defer timeout.Stop()
200-
201- for {
202- select {
203- case result := <-resultChan:
204- if !result.Success {
205- wm.paneCloseLogf("async close failed node=%p err=%v", node, result.Error)
206- wm.dumpTreeState("after_close_error")
207- if tombstone != nil {
208- if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
209- log.Printf("[workspace] Rollback failed after close failure: %v", rollbackErr)
210- }
211 }173 }
212- return result.Error
213 }174 }
214-175+ } else {
215- if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {176+ if verr := wm.treeValidator.ValidateTree(wm.root, "after_close"); verr != nil {
216- log.Printf("[workspace] Tree validation failed after close: %v", err)177+ log.Printf("[workspace] Tree validation failed after close: %v", verr)
217 }178 }
218- wm.paneCloseLogf("async close succeeded node=%p promoted=%p root=%p", node, result.NewNode, wm.root)179+ wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root)
219 wm.dumpTreeState("after_close_success")180 wm.dumpTreeState("after_close_success")
220 181
221 if wm.treeRebalancer != nil {182 if wm.treeRebalancer != nil {
222- if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil {183+ if rerr := wm.treeRebalancer.RebalanceAfterClose(node, promoted); rerr != nil {
223- log.Printf("[workspace] Tree rebalancing failed after close: %v", err)184+ log.Printf("[workspace] Tree rebalancing failed after close: %v", rerr)
224 }185 }
225 }186 }
187+ }
188+ close(done)
189+ return false
190+ })
226 191
227- log.Printf("[workspace] Close operation completed successfully")192+ <-done
228- return nil
229-
230- case <-ticker.C:
231- if webkit.IsMainThread() {
232- webkit.IterateMainLoop()
233- }
234 193
235- case <-timeout.C:194+ if closeErr != nil {
236- return fmt.Errorf("close operation timed out")195+ return closeErr
237- }
238 }196 }
197+
198+ log.Printf("[workspace] Close operation completed successfully")
199+ return nil
239 }200 }
240 201
241 // StackPane performs a stack operation with validation and safety checks202 // StackPane performs a stack operation with validation and safety checks
@@ -275,7 +236,7 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
275 if err != nil {236 if err != nil {
276 if tombstone != nil {237 if tombstone != nil {
277 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {238 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
278- log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr)239+ log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr)
279 }240 }
280 }241 }
281 return nil, err242 return nil, err
@@ -289,53 +250,37 @@ func (wm *WorkspaceManager) StackPane(target *paneNode) (*paneNode, error) {
289 return newNode, nil250 return newNode, nil
290 }251 }
291 252
292- // Step 5: Not on main thread, marshal through concurrency controller253+ // Step 5: Not on main thread, marshal via IdleAdd
293- opReq := &OperationRequest{254+ log.Printf("[workspace] Not on main thread, marshalling stack via IdleAdd")
294- ID: fmt.Sprintf("stack_%p_%d", target, time.Now().UnixNano()),255+ var newNode *paneNode
295- Type: OpTypeStack,256+ var stackErr error
296- TargetNode: target,257+ done := make(chan struct{})
297- Parameters: map[string]interface{}{},
298- Context: context.Background(),
299- MaxRetries: 3,
300- }
301-
302- resultChan := wm.concurrencyController.SubmitOperation(opReq)
303-
304- log.Printf("[workspace] Waiting for operation result while pumping GTK events")
305- ticker := time.NewTicker(10 * time.Millisecond)
306- defer ticker.Stop()
307 258
308- timeout := time.NewTimer(30 * time.Second)259+ webkit.IdleAdd(func() bool {
309- defer timeout.Stop()260+ newNode, stackErr = wm.stackedPaneManager.StackPane(target)
310-261+ if stackErr != nil {
311- for {262+ if tombstone != nil {
312- select {263+ if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
313- case result := <-resultChan:264+ log.Printf("[workspace] Rollback failed after stack failure: %v", rollbackErr)
314- if !result.Success {
315- if tombstone != nil {
316- if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
317- log.Printf("[bulletproof] Rollback failed after stack failure: %v", rollbackErr)
318- }
319 }265 }
320- return nil, result.Error
321 }266 }
322-267+ } else {
323- if err := wm.treeValidator.ValidateTree(wm.root, "after_stack"); err != nil {268+ if verr := wm.treeValidator.ValidateTree(wm.root, "after_stack"); verr != nil {
324- log.Printf("[workspace] Tree validation failed after stack: %v", err)269+ log.Printf("[workspace] Tree validation failed after stack: %v", verr)
325 }270 }
271+ }
272+ close(done)
273+ return false
274+ })
326 275
327- log.Printf("[workspace] Stack operation completed successfully: newNode=%p", result.NewNode)276+ <-done
328- return result.NewNode, nil
329-
330- case <-ticker.C:
331- if webkit.IsMainThread() {
332- webkit.IterateMainLoop()
333- }
334 277
335- case <-timeout.C:278+ if stackErr != nil {
336- return nil, fmt.Errorf("stack operation timed out")279+ return nil, stackErr
337- }
338 }280 }
281+
282+ log.Printf("[workspace] Stack operation completed successfully: newNode=%p", newNode)
283+ return newNode, nil
339 }284 }
340 285
341 // EnableEnhancedMode enables all enhanced validation features286 // EnableEnhancedMode enables all enhanced validation features
@@ -383,9 +328,6 @@ func (wm *WorkspaceManager) GetEnhancedStats() map[string]interface{} {
383 stats["widget_transactions"] = wm.widgetTxManager.GetTransactionStats()328 stats["widget_transactions"] = wm.widgetTxManager.GetTransactionStats()
384 }329 }
385 330
386- if wm.concurrencyController != nil {
387- stats["concurrency"] = wm.concurrencyController.GetConcurrencyStats()
388- }
389 331
390 if wm.treeRebalancer != nil {332 if wm.treeRebalancer != nil {
391 stats["tree_rebalancing"] = wm.treeRebalancer.GetRebalancingStats()333 stats["tree_rebalancing"] = wm.treeRebalancer.GetRebalancingStats()
@@ -432,9 +374,6 @@ func (wm *WorkspaceManager) ValidateWorkspaceIntegrity() error {
432 func (wm *WorkspaceManager) ShutdownEnhancedComponents() {374 func (wm *WorkspaceManager) ShutdownEnhancedComponents() {
433 log.Printf("[workspace] Shutting down enhanced validation components")375 log.Printf("[workspace] Shutting down enhanced validation components")
434 376
435- if wm.concurrencyController != nil {
436- wm.concurrencyController.Shutdown()
437- }
438 377
439 // Other components don't require explicit shutdown currently378 // Other components don't require explicit shutdown currently
440 log.Printf("[workspace] Enhanced validation components shutdown complete")379 log.Printf("[workspace] Enhanced validation components shutdown complete")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close