Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

242 rindas
5.7KB

  1. package output
  2. import (
  3. "context"
  4. "errors"
  5. "sync"
  6. )
  7. // ErrFrameQueueClosed is returned when a queue operation is attempted after the queue
  8. // has been closed.
  9. var ErrFrameQueueClosed = errors.New("frame queue closed")
  10. // QueueStats exposes the runtime state of a frame queue.
  11. type QueueStats struct {
  12. Capacity int `json:"capacity"`
  13. Depth int `json:"depth"`
  14. FillLevel float64 `json:"fillLevel"`
  15. Health QueueHealth `json:"health"`
  16. HighWaterMark int `json:"highWaterMark"`
  17. LowWaterMark int `json:"lowWaterMark"`
  18. PushTimeouts uint64 `json:"pushTimeouts"`
  19. PopTimeouts uint64 `json:"popTimeouts"`
  20. DroppedFrames uint64 `json:"droppedFrames"`
  21. RepeatedFrames uint64 `json:"repeatedFrames"`
  22. MutedFrames uint64 `json:"mutedFrames"`
  23. }
  24. type QueueHealth string
  25. const (
  26. QueueHealthCritical QueueHealth = "critical"
  27. QueueHealthLow QueueHealth = "low"
  28. QueueHealthNormal QueueHealth = "normal"
  29. )
  30. const (
  31. queueHealthCriticalThreshold = 0.2
  32. queueHealthLowThreshold = 0.5
  33. )
  34. // FrameQueue is a bounded ring that holds CompositeFrame instances between the
  35. // generator and the writer. Push blocks when the queue is full until space
  36. // becomes available or the provided context is cancelled. Pop blocks when the
  37. // queue is empty until a new frame arrives or the context is cancelled.
  38. type FrameQueue struct {
  39. capacity int
  40. ch chan *CompositeFrame
  41. mu sync.Mutex
  42. depth int
  43. highWaterMark int
  44. lowWaterMark int
  45. pushTimeouts uint64
  46. popTimeouts uint64
  47. dropped uint64
  48. repeated uint64
  49. muted uint64
  50. closed bool
  51. closeOnce sync.Once
  52. }
  53. // NewFrameQueue builds a bounded queue that holds up to capacity frames.
  54. func NewFrameQueue(capacity int) *FrameQueue {
  55. if capacity <= 0 {
  56. capacity = 1
  57. }
  58. fq := &FrameQueue{
  59. capacity: capacity,
  60. ch: make(chan *CompositeFrame, capacity),
  61. lowWaterMark: capacity,
  62. }
  63. fq.trackDepth(0)
  64. return fq
  65. }
  66. // Capacity returns the fixed frame capacity of the queue.
  67. func (q *FrameQueue) Capacity() int {
  68. return q.capacity
  69. }
  70. // FillLevel reports the current occupancy as a fraction of capacity.
  71. // Uses len(ch) directly for accuracy: updateDepth() is called after the
  72. // channel operation, so q.depth can lag by one frame transiently.
  73. func (q *FrameQueue) FillLevel() float64 {
  74. if q.capacity == 0 {
  75. return 0
  76. }
  77. return float64(len(q.ch)) / float64(q.capacity)
  78. }
  79. // Depth returns the current number of frames in the queue.
  80. // Uses len(ch) directly for accuracy (see FillLevel).
  81. func (q *FrameQueue) Depth() int {
  82. return len(q.ch)
  83. }
  84. // Stats returns a snapshot of the queue metrics.
  85. func (q *FrameQueue) Stats() QueueStats {
  86. q.mu.Lock()
  87. fill := q.fillLevelLocked()
  88. stats := QueueStats{
  89. Capacity: q.capacity,
  90. Depth: len(q.ch),
  91. FillLevel: fill,
  92. Health: queueHealthFromFill(fill),
  93. HighWaterMark: q.highWaterMark,
  94. LowWaterMark: q.lowWaterMark,
  95. PushTimeouts: q.pushTimeouts,
  96. PopTimeouts: q.popTimeouts,
  97. DroppedFrames: q.dropped,
  98. RepeatedFrames: q.repeated,
  99. MutedFrames: q.muted,
  100. }
  101. q.mu.Unlock()
  102. return stats
  103. }
  104. // Push enqueues a frame, blocking until space is available or ctx is done.
  105. func (q *FrameQueue) Push(ctx context.Context, frame *CompositeFrame) error {
  106. if frame == nil {
  107. return errors.New("frame required")
  108. }
  109. if q.isClosed() {
  110. return ErrFrameQueueClosed
  111. }
  112. // BUG-05 fix: increment depth BEFORE the channel send so that Stats()
  113. // never reports fill=0 while a frame is in the channel awaiting receive.
  114. // On context cancellation, undo the increment.
  115. q.updateDepth(+1)
  116. select {
  117. case q.ch <- frame:
  118. return nil
  119. case <-ctx.Done():
  120. q.updateDepth(-1)
  121. q.recordPushTimeout()
  122. return ctx.Err()
  123. }
  124. }
  125. // Pop removes a frame, blocking until one is available or ctx signals done.
  126. func (q *FrameQueue) Pop(ctx context.Context) (*CompositeFrame, error) {
  127. select {
  128. case frame, ok := <-q.ch:
  129. if !ok {
  130. return nil, ErrFrameQueueClosed
  131. }
  132. q.updateDepth(-1)
  133. return frame, nil
  134. case <-ctx.Done():
  135. q.recordPopTimeout()
  136. return nil, ctx.Err()
  137. }
  138. }
  139. // Close marks the queue as closed and wakes up blocked callers.
  140. func (q *FrameQueue) Close() {
  141. q.closeOnce.Do(func() {
  142. q.mu.Lock()
  143. q.closed = true
  144. q.mu.Unlock()
  145. close(q.ch)
  146. })
  147. }
  148. // RecordDrop increments the drop counter for instrumentation.
  149. func (q *FrameQueue) RecordDrop() {
  150. q.mu.Lock()
  151. q.dropped++
  152. q.mu.Unlock()
  153. }
  154. // RecordRepeat increments the repeat counter for instrumentation.
  155. func (q *FrameQueue) RecordRepeat() {
  156. q.mu.Lock()
  157. q.repeated++
  158. q.mu.Unlock()
  159. }
  160. // RecordMute increments the mute counter for instrumentation.
  161. func (q *FrameQueue) RecordMute() {
  162. q.mu.Lock()
  163. q.muted++
  164. q.mu.Unlock()
  165. }
  166. func (q *FrameQueue) isClosed() bool {
  167. q.mu.Lock()
  168. closed := q.closed
  169. q.mu.Unlock()
  170. return closed
  171. }
  172. func (q *FrameQueue) updateDepth(delta int) {
  173. q.mu.Lock()
  174. q.depth += delta
  175. q.trackDepth(q.depth)
  176. q.mu.Unlock()
  177. }
  178. func (q *FrameQueue) trackDepth(depth int) {
  179. if depth > q.highWaterMark {
  180. q.highWaterMark = depth
  181. }
  182. if depth < q.lowWaterMark {
  183. q.lowWaterMark = depth
  184. }
  185. }
  186. func (q *FrameQueue) fillLevelLocked() float64 {
  187. if q.capacity == 0 {
  188. return 0
  189. }
  190. // Use len(ch) rather than q.depth: depth is updated after the channel
  191. // operation, so it can be off by one during the Push/Pop window.
  192. return float64(len(q.ch)) / float64(q.capacity)
  193. }
  194. func (q *FrameQueue) recordPushTimeout() {
  195. q.mu.Lock()
  196. q.pushTimeouts++
  197. q.mu.Unlock()
  198. }
  199. func (q *FrameQueue) recordPopTimeout() {
  200. q.mu.Lock()
  201. q.popTimeouts++
  202. q.mu.Unlock()
  203. }
  204. func queueHealthFromFill(fill float64) QueueHealth {
  205. switch {
  206. case fill <= queueHealthCriticalThreshold:
  207. return QueueHealthCritical
  208. case fill <= queueHealthLowThreshold:
  209. return QueueHealthLow
  210. default:
  211. return QueueHealthNormal
  212. }
  213. }