Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

286 lignes
8.0KB

  1. package pipeline
  2. import (
  3. "sort"
  4. "time"
  5. )
  6. type DecisionQueueStats struct {
  7. RecordQueued int `json:"record_queued"`
  8. DecodeQueued int `json:"decode_queued"`
  9. RecordSelected int `json:"record_selected"`
  10. DecodeSelected int `json:"decode_selected"`
  11. RecordActive int `json:"record_active"`
  12. DecodeActive int `json:"decode_active"`
  13. RecordOldestS float64 `json:"record_oldest_sec"`
  14. DecodeOldestS float64 `json:"decode_oldest_sec"`
  15. RecordBudget int `json:"record_budget"`
  16. DecodeBudget int `json:"decode_budget"`
  17. HoldMs int `json:"hold_ms"`
  18. RecordHoldMs int `json:"record_hold_ms"`
  19. DecodeHoldMs int `json:"decode_hold_ms"`
  20. RecordDropped int `json:"record_dropped"`
  21. DecodeDropped int `json:"decode_dropped"`
  22. }
  23. type queuedDecision struct {
  24. ID int64
  25. SNRDb float64
  26. Hint string
  27. Class string
  28. FirstSeen time.Time
  29. LastSeen time.Time
  30. }
  31. type queueSelection struct {
  32. selected map[int64]struct{}
  33. held map[int64]struct{}
  34. scores map[int64]float64
  35. minScore float64
  36. maxScore float64
  37. cutoff float64
  38. }
  39. type decisionQueues struct {
  40. record map[int64]*queuedDecision
  41. decode map[int64]*queuedDecision
  42. recordHold map[int64]time.Time
  43. decodeHold map[int64]time.Time
  44. }
  45. func newDecisionQueues() *decisionQueues {
  46. return &decisionQueues{
  47. record: map[int64]*queuedDecision{},
  48. decode: map[int64]*queuedDecision{},
  49. recordHold: map[int64]time.Time{},
  50. decodeHold: map[int64]time.Time{},
  51. }
  52. }
  53. func (dq *decisionQueues) Apply(decisions []SignalDecision, budget BudgetModel, now time.Time, policy Policy) DecisionQueueStats {
  54. if dq == nil {
  55. return DecisionQueueStats{}
  56. }
  57. holdPolicy := HoldPolicyFromPolicy(policy)
  58. recordHold := time.Duration(holdPolicy.RecordMs) * time.Millisecond
  59. decodeHold := time.Duration(holdPolicy.DecodeMs) * time.Millisecond
  60. recSeen := map[int64]bool{}
  61. decSeen := map[int64]bool{}
  62. for i := range decisions {
  63. id := decisions[i].Candidate.ID
  64. if id == 0 {
  65. continue
  66. }
  67. if decisions[i].ShouldRecord {
  68. qd := dq.record[id]
  69. if qd == nil {
  70. qd = &queuedDecision{ID: id, FirstSeen: now}
  71. dq.record[id] = qd
  72. }
  73. qd.SNRDb = decisions[i].Candidate.SNRDb
  74. qd.Hint = decisions[i].Candidate.Hint
  75. qd.Class = decisions[i].Class
  76. qd.LastSeen = now
  77. recSeen[id] = true
  78. }
  79. if decisions[i].ShouldAutoDecode {
  80. qd := dq.decode[id]
  81. if qd == nil {
  82. qd = &queuedDecision{ID: id, FirstSeen: now}
  83. dq.decode[id] = qd
  84. }
  85. qd.SNRDb = decisions[i].Candidate.SNRDb
  86. qd.Hint = decisions[i].Candidate.Hint
  87. qd.Class = decisions[i].Class
  88. qd.LastSeen = now
  89. decSeen[id] = true
  90. }
  91. }
  92. for id := range dq.record {
  93. if !recSeen[id] {
  94. delete(dq.record, id)
  95. }
  96. }
  97. for id := range dq.decode {
  98. if !decSeen[id] {
  99. delete(dq.decode, id)
  100. }
  101. }
  102. purgeExpired(dq.recordHold, now)
  103. purgeExpired(dq.decodeHold, now)
  104. recSelected := selectQueued("record", dq.record, dq.recordHold, budget.Record.Max, recordHold, now, policy)
  105. decSelected := selectQueued("decode", dq.decode, dq.decodeHold, budget.Decode.Max, decodeHold, now, policy)
  106. stats := DecisionQueueStats{
  107. RecordQueued: len(dq.record),
  108. DecodeQueued: len(dq.decode),
  109. RecordSelected: len(recSelected.selected),
  110. DecodeSelected: len(decSelected.selected),
  111. RecordActive: len(dq.recordHold),
  112. DecodeActive: len(dq.decodeHold),
  113. RecordOldestS: oldestAge(dq.record, now),
  114. DecodeOldestS: oldestAge(dq.decode, now),
  115. RecordBudget: budget.Record.Max,
  116. DecodeBudget: budget.Decode.Max,
  117. HoldMs: budget.HoldMs,
  118. RecordHoldMs: holdPolicy.RecordMs,
  119. DecodeHoldMs: holdPolicy.DecodeMs,
  120. }
  121. for i := range decisions {
  122. id := decisions[i].Candidate.ID
  123. if decisions[i].ShouldRecord {
  124. decisions[i].RecordAdmission = buildQueueAdmission("record", id, recSelected, policy, holdPolicy, budget.Record.Source)
  125. if _, ok := recSelected.selected[id]; !ok {
  126. decisions[i].ShouldRecord = false
  127. decisions[i].Reason = admissionReason(DecisionReasonQueueRecord, policy, holdPolicy, "pressure:budget", "budget:"+slugToken(budget.Record.Source))
  128. stats.RecordDropped++
  129. }
  130. }
  131. if decisions[i].ShouldAutoDecode {
  132. decisions[i].DecodeAdmission = buildQueueAdmission("decode", id, decSelected, policy, holdPolicy, budget.Decode.Source)
  133. if _, ok := decSelected.selected[id]; !ok {
  134. decisions[i].ShouldAutoDecode = false
  135. if decisions[i].Reason == "" {
  136. decisions[i].Reason = admissionReason(DecisionReasonQueueDecode, policy, holdPolicy, "pressure:budget", "budget:"+slugToken(budget.Decode.Source))
  137. }
  138. stats.DecodeDropped++
  139. }
  140. }
  141. }
  142. return stats
  143. }
  144. func selectQueued(queueName string, queue map[int64]*queuedDecision, hold map[int64]time.Time, max int, holdDur time.Duration, now time.Time, policy Policy) queueSelection {
  145. selection := queueSelection{
  146. selected: map[int64]struct{}{},
  147. held: map[int64]struct{}{},
  148. scores: map[int64]float64{},
  149. }
  150. if len(queue) == 0 {
  151. return selection
  152. }
  153. type scored struct {
  154. id int64
  155. score float64
  156. }
  157. scoredList := make([]scored, 0, len(queue))
  158. for id, qd := range queue {
  159. age := now.Sub(qd.FirstSeen).Seconds()
  160. boost := age / 2.0
  161. if boost > 5 {
  162. boost = 5
  163. }
  164. hint := qd.Hint
  165. if hint == "" {
  166. hint = qd.Class
  167. }
  168. policyBoost := DecisionPriorityBoost(policy, hint, qd.Class, queueName)
  169. score := qd.SNRDb + boost + policyBoost
  170. selection.scores[id] = score
  171. if len(scoredList) == 0 || score < selection.minScore {
  172. selection.minScore = score
  173. }
  174. if len(scoredList) == 0 || score > selection.maxScore {
  175. selection.maxScore = score
  176. }
  177. scoredList = append(scoredList, scored{id: id, score: score})
  178. }
  179. sort.Slice(scoredList, func(i, j int) bool {
  180. return scoredList[i].score > scoredList[j].score
  181. })
  182. limit := max
  183. if limit <= 0 || limit > len(scoredList) {
  184. limit = len(scoredList)
  185. }
  186. if len(hold) > 0 && len(hold) > limit {
  187. limit = len(hold)
  188. if limit > len(scoredList) {
  189. limit = len(scoredList)
  190. }
  191. }
  192. for id := range hold {
  193. if _, ok := queue[id]; ok {
  194. selection.selected[id] = struct{}{}
  195. selection.held[id] = struct{}{}
  196. }
  197. }
  198. for _, s := range scoredList {
  199. if len(selection.selected) >= limit {
  200. break
  201. }
  202. if _, ok := selection.selected[s.id]; ok {
  203. continue
  204. }
  205. selection.selected[s.id] = struct{}{}
  206. }
  207. if holdDur > 0 {
  208. for id := range selection.selected {
  209. hold[id] = now.Add(holdDur)
  210. }
  211. }
  212. if len(selection.selected) > 0 {
  213. first := true
  214. for id := range selection.selected {
  215. score := selection.scores[id]
  216. if first || score < selection.cutoff {
  217. selection.cutoff = score
  218. first = false
  219. }
  220. }
  221. }
  222. return selection
  223. }
  224. func buildQueueAdmission(queueName string, id int64, selection queueSelection, policy Policy, holdPolicy HoldPolicy, budgetSource string) *PriorityAdmission {
  225. score, ok := selection.scores[id]
  226. if !ok {
  227. return nil
  228. }
  229. admission := &PriorityAdmission{
  230. Basis: queueName,
  231. Score: score,
  232. Cutoff: selection.cutoff,
  233. Tier: PriorityTierFromRange(score, selection.minScore, selection.maxScore),
  234. }
  235. if _, ok := selection.selected[id]; ok {
  236. if _, held := selection.held[id]; held {
  237. admission.Class = AdmissionClassHold
  238. admission.Reason = admissionReason("queue:"+queueName+":hold", policy, holdPolicy, "pressure:hold", "budget:"+slugToken(budgetSource))
  239. } else {
  240. admission.Class = AdmissionClassAdmit
  241. admission.Reason = admissionReason("queue:"+queueName+":admit", policy, holdPolicy, "budget:"+slugToken(budgetSource))
  242. }
  243. return admission
  244. }
  245. admission.Class = AdmissionClassDefer
  246. admission.Reason = admissionReason("queue:"+queueName+":budget", policy, holdPolicy, "pressure:budget", "budget:"+slugToken(budgetSource))
  247. return admission
  248. }
  249. func purgeExpired(hold map[int64]time.Time, now time.Time) {
  250. for id, until := range hold {
  251. if now.After(until) {
  252. delete(hold, id)
  253. }
  254. }
  255. }
  256. func oldestAge(queue map[int64]*queuedDecision, now time.Time) float64 {
  257. oldest := 0.0
  258. first := true
  259. for _, qd := range queue {
  260. age := now.Sub(qd.FirstSeen).Seconds()
  261. if first || age > oldest {
  262. oldest = age
  263. first = false
  264. }
  265. }
  266. if first {
  267. return 0
  268. }
  269. return oldest
  270. }