Wideband autonomous SDR analysis engine forked from sdr-visual-suite
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

431 рядки
14KB

  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. RecordHoldSelected int `json:"record_hold_selected"`
  23. DecodeHoldSelected int `json:"decode_hold_selected"`
  24. RecordHoldProtected int `json:"record_hold_protected"`
  25. DecodeHoldProtected int `json:"decode_hold_protected"`
  26. RecordHoldExpired int `json:"record_hold_expired"`
  27. DecodeHoldExpired int `json:"decode_hold_expired"`
  28. RecordHoldDisplaced int `json:"record_hold_displaced"`
  29. DecodeHoldDisplaced int `json:"decode_hold_displaced"`
  30. RecordOpportunistic int `json:"record_opportunistic"`
  31. DecodeOpportunistic int `json:"decode_opportunistic"`
  32. RecordDisplaced int `json:"record_displaced"`
  33. DecodeDisplaced int `json:"decode_displaced"`
  34. }
  35. type queuedDecision struct {
  36. ID int64
  37. SNRDb float64
  38. Hint string
  39. Class string
  40. FirstSeen time.Time
  41. LastSeen time.Time
  42. }
  43. type queueSelection struct {
  44. selected map[int64]struct{}
  45. held map[int64]struct{}
  46. protected map[int64]struct{}
  47. displacedByHold map[int64]struct{}
  48. displaced map[int64]struct{}
  49. opportunistic map[int64]struct{}
  50. expired map[int64]struct{}
  51. scores map[int64]float64
  52. tiers map[int64]string
  53. minScore float64
  54. maxScore float64
  55. cutoff float64
  56. }
  57. type decisionQueues struct {
  58. record map[int64]*queuedDecision
  59. decode map[int64]*queuedDecision
  60. recordHold map[int64]time.Time
  61. decodeHold map[int64]time.Time
  62. }
  63. func newDecisionQueues() *decisionQueues {
  64. return &decisionQueues{
  65. record: map[int64]*queuedDecision{},
  66. decode: map[int64]*queuedDecision{},
  67. recordHold: map[int64]time.Time{},
  68. decodeHold: map[int64]time.Time{},
  69. }
  70. }
  71. func (dq *decisionQueues) Apply(decisions []SignalDecision, budget BudgetModel, now time.Time, policy Policy) DecisionQueueStats {
  72. if dq == nil {
  73. return DecisionQueueStats{}
  74. }
  75. holdPolicy := HoldPolicyFromPolicy(policy)
  76. recordHold := time.Duration(holdPolicy.RecordMs) * time.Millisecond
  77. decodeHold := time.Duration(holdPolicy.DecodeMs) * time.Millisecond
  78. recSeen := map[int64]bool{}
  79. decSeen := map[int64]bool{}
  80. for i := range decisions {
  81. id := decisions[i].Candidate.ID
  82. if id == 0 {
  83. continue
  84. }
  85. if decisions[i].ShouldRecord {
  86. qd := dq.record[id]
  87. if qd == nil {
  88. qd = &queuedDecision{ID: id, FirstSeen: now}
  89. dq.record[id] = qd
  90. }
  91. qd.SNRDb = decisions[i].Candidate.SNRDb
  92. qd.Hint = decisions[i].Candidate.Hint
  93. qd.Class = decisions[i].Class
  94. qd.LastSeen = now
  95. recSeen[id] = true
  96. }
  97. if decisions[i].ShouldAutoDecode {
  98. qd := dq.decode[id]
  99. if qd == nil {
  100. qd = &queuedDecision{ID: id, FirstSeen: now}
  101. dq.decode[id] = qd
  102. }
  103. qd.SNRDb = decisions[i].Candidate.SNRDb
  104. qd.Hint = decisions[i].Candidate.Hint
  105. qd.Class = decisions[i].Class
  106. qd.LastSeen = now
  107. decSeen[id] = true
  108. }
  109. }
  110. for id := range dq.record {
  111. if !recSeen[id] {
  112. delete(dq.record, id)
  113. }
  114. }
  115. for id := range dq.decode {
  116. if !decSeen[id] {
  117. delete(dq.decode, id)
  118. }
  119. }
  120. recExpired := expireHold(dq.recordHold, now)
  121. decExpired := expireHold(dq.decodeHold, now)
  122. recSelected := selectQueued("record", dq.record, dq.recordHold, budget.Record.Max, recordHold, now, policy, recExpired)
  123. decSelected := selectQueued("decode", dq.decode, dq.decodeHold, budget.Decode.Max, decodeHold, now, policy, decExpired)
  124. recPressure := buildQueuePressure(budget.Record, len(dq.record), len(recSelected.selected), len(dq.recordHold))
  125. decPressure := buildQueuePressure(budget.Decode, len(dq.decode), len(decSelected.selected), len(dq.decodeHold))
  126. recPressureTag := pressureReasonTag(recPressure)
  127. decPressureTag := pressureReasonTag(decPressure)
  128. stats := DecisionQueueStats{
  129. RecordQueued: len(dq.record),
  130. DecodeQueued: len(dq.decode),
  131. RecordSelected: len(recSelected.selected),
  132. DecodeSelected: len(decSelected.selected),
  133. RecordActive: len(dq.recordHold),
  134. DecodeActive: len(dq.decodeHold),
  135. RecordOldestS: oldestAge(dq.record, now),
  136. DecodeOldestS: oldestAge(dq.decode, now),
  137. RecordBudget: budget.Record.Max,
  138. DecodeBudget: budget.Decode.Max,
  139. HoldMs: budget.HoldMs,
  140. RecordHoldMs: holdPolicy.RecordMs,
  141. DecodeHoldMs: holdPolicy.DecodeMs,
  142. RecordHoldSelected: len(recSelected.held) - len(recSelected.displaced),
  143. DecodeHoldSelected: len(decSelected.held) - len(decSelected.displaced),
  144. RecordHoldProtected: len(recSelected.protected),
  145. DecodeHoldProtected: len(decSelected.protected),
  146. RecordHoldExpired: len(recExpired),
  147. DecodeHoldExpired: len(decExpired),
  148. RecordHoldDisplaced: len(recSelected.displaced),
  149. DecodeHoldDisplaced: len(decSelected.displaced),
  150. RecordOpportunistic: len(recSelected.opportunistic),
  151. DecodeOpportunistic: len(decSelected.opportunistic),
  152. RecordDisplaced: len(recSelected.displacedByHold),
  153. DecodeDisplaced: len(decSelected.displacedByHold),
  154. }
  155. for i := range decisions {
  156. id := decisions[i].Candidate.ID
  157. if decisions[i].ShouldRecord {
  158. decisions[i].RecordAdmission = buildQueueAdmission("record", id, recSelected, policy, holdPolicy, budget.Record.Source, recPressureTag)
  159. if _, ok := recSelected.selected[id]; !ok {
  160. decisions[i].ShouldRecord = false
  161. extras := []string{recPressureTag, "pressure:budget", "budget:" + slugToken(budget.Record.Source)}
  162. if _, ok := recSelected.displaced[id]; ok {
  163. extras = []string{recPressureTag, "pressure:hold", ReasonTagDisplaceOpportunist, ReasonTagDisplaceTier, ReasonTagHoldDisplaced, "budget:" + slugToken(budget.Record.Source)}
  164. } else if _, ok := recSelected.displacedByHold[id]; ok {
  165. extras = []string{recPressureTag, "pressure:hold", ReasonTagHoldActive, "budget:" + slugToken(budget.Record.Source)}
  166. } else if _, ok := recSelected.expired[id]; ok {
  167. extras = append(extras, ReasonTagHoldExpired)
  168. }
  169. decisions[i].Reason = admissionReason(DecisionReasonQueueRecord, policy, holdPolicy, extras...)
  170. stats.RecordDropped++
  171. }
  172. }
  173. if decisions[i].ShouldAutoDecode {
  174. decisions[i].DecodeAdmission = buildQueueAdmission("decode", id, decSelected, policy, holdPolicy, budget.Decode.Source, decPressureTag)
  175. if _, ok := decSelected.selected[id]; !ok {
  176. decisions[i].ShouldAutoDecode = false
  177. if decisions[i].Reason == "" {
  178. extras := []string{decPressureTag, "pressure:budget", "budget:" + slugToken(budget.Decode.Source)}
  179. if _, ok := decSelected.displaced[id]; ok {
  180. extras = []string{decPressureTag, "pressure:hold", ReasonTagDisplaceOpportunist, ReasonTagDisplaceTier, ReasonTagHoldDisplaced, "budget:" + slugToken(budget.Decode.Source)}
  181. } else if _, ok := decSelected.displacedByHold[id]; ok {
  182. extras = []string{decPressureTag, "pressure:hold", ReasonTagHoldActive, "budget:" + slugToken(budget.Decode.Source)}
  183. } else if _, ok := decSelected.expired[id]; ok {
  184. extras = append(extras, ReasonTagHoldExpired)
  185. }
  186. decisions[i].Reason = admissionReason(DecisionReasonQueueDecode, policy, holdPolicy, extras...)
  187. }
  188. stats.DecodeDropped++
  189. }
  190. }
  191. }
  192. return stats
  193. }
  194. func selectQueued(queueName string, queue map[int64]*queuedDecision, hold map[int64]time.Time, max int, holdDur time.Duration, now time.Time, policy Policy, expired map[int64]struct{}) queueSelection {
  195. selection := queueSelection{
  196. selected: map[int64]struct{}{},
  197. held: map[int64]struct{}{},
  198. protected: map[int64]struct{}{},
  199. displacedByHold: map[int64]struct{}{},
  200. displaced: map[int64]struct{}{},
  201. opportunistic: map[int64]struct{}{},
  202. expired: map[int64]struct{}{},
  203. scores: map[int64]float64{},
  204. tiers: map[int64]string{},
  205. }
  206. if len(queue) == 0 {
  207. return selection
  208. }
  209. for id := range expired {
  210. selection.expired[id] = struct{}{}
  211. }
  212. type scored struct {
  213. id int64
  214. score float64
  215. }
  216. scoredList := make([]scored, 0, len(queue))
  217. for id, qd := range queue {
  218. age := now.Sub(qd.FirstSeen).Seconds()
  219. boost := age / 2.0
  220. if boost > 5 {
  221. boost = 5
  222. }
  223. hint := qd.Hint
  224. if hint == "" {
  225. hint = qd.Class
  226. }
  227. policyBoost := DecisionPriorityBoost(policy, hint, qd.Class, queueName)
  228. score := qd.SNRDb + boost + policyBoost
  229. selection.scores[id] = score
  230. if len(scoredList) == 0 || score < selection.minScore {
  231. selection.minScore = score
  232. }
  233. if len(scoredList) == 0 || score > selection.maxScore {
  234. selection.maxScore = score
  235. }
  236. scoredList = append(scoredList, scored{id: id, score: score})
  237. }
  238. sort.Slice(scoredList, func(i, j int) bool {
  239. return scoredList[i].score > scoredList[j].score
  240. })
  241. for id, score := range selection.scores {
  242. selection.tiers[id] = PriorityTierFromRange(score, selection.minScore, selection.maxScore)
  243. }
  244. limit := max
  245. if limit <= 0 || limit > len(scoredList) {
  246. limit = len(scoredList)
  247. }
  248. if len(hold) > 0 && len(hold) > limit {
  249. limit = len(hold)
  250. if limit > len(scoredList) {
  251. limit = len(scoredList)
  252. }
  253. }
  254. for id := range hold {
  255. if _, ok := queue[id]; ok {
  256. selection.selected[id] = struct{}{}
  257. selection.held[id] = struct{}{}
  258. if isProtectedTier(selection.tiers[id]) {
  259. selection.protected[id] = struct{}{}
  260. }
  261. }
  262. }
  263. displaceable := buildDisplaceableHold(selection.held, selection.protected, selection.tiers, selection.scores)
  264. for _, s := range scoredList {
  265. if _, ok := selection.selected[s.id]; ok {
  266. continue
  267. }
  268. if len(selection.selected) < limit {
  269. selection.selected[s.id] = struct{}{}
  270. continue
  271. }
  272. if len(displaceable) == 0 {
  273. continue
  274. }
  275. target := displaceable[0]
  276. if priorityTierRank(selection.tiers[s.id]) <= priorityTierRank(selection.tiers[target]) {
  277. continue
  278. }
  279. displaceable = displaceable[1:]
  280. delete(selection.selected, target)
  281. selection.displaced[target] = struct{}{}
  282. selection.selected[s.id] = struct{}{}
  283. selection.opportunistic[s.id] = struct{}{}
  284. }
  285. if holdDur > 0 {
  286. for id := range selection.displaced {
  287. delete(hold, id)
  288. }
  289. for id := range selection.selected {
  290. hold[id] = now.Add(holdDur)
  291. }
  292. }
  293. if len(selection.selected) > 0 {
  294. first := true
  295. for id := range selection.selected {
  296. score := selection.scores[id]
  297. if first || score < selection.cutoff {
  298. selection.cutoff = score
  299. first = false
  300. }
  301. }
  302. }
  303. if len(selection.selected) > 0 {
  304. for id := range selection.scores {
  305. if _, ok := selection.selected[id]; ok {
  306. continue
  307. }
  308. if _, ok := selection.displaced[id]; ok {
  309. continue
  310. }
  311. if selection.scores[id] >= selection.cutoff {
  312. selection.displacedByHold[id] = struct{}{}
  313. }
  314. }
  315. }
  316. return selection
  317. }
  318. func buildDisplaceableHold(held map[int64]struct{}, protected map[int64]struct{}, tiers map[int64]string, scores map[int64]float64) []int64 {
  319. type entry struct {
  320. id int64
  321. rank int
  322. score float64
  323. }
  324. candidates := make([]entry, 0, len(held))
  325. for id := range held {
  326. if _, ok := protected[id]; ok {
  327. continue
  328. }
  329. score := 0.0
  330. if scores != nil {
  331. score = scores[id]
  332. }
  333. candidates = append(candidates, entry{
  334. id: id,
  335. rank: priorityTierRank(tiers[id]),
  336. score: score,
  337. })
  338. }
  339. if len(candidates) == 0 {
  340. return nil
  341. }
  342. sort.Slice(candidates, func(i, j int) bool {
  343. if candidates[i].rank == candidates[j].rank {
  344. return candidates[i].score < candidates[j].score
  345. }
  346. return candidates[i].rank < candidates[j].rank
  347. })
  348. out := make([]int64, 0, len(candidates))
  349. for _, c := range candidates {
  350. out = append(out, c.id)
  351. }
  352. return out
  353. }
  354. func buildQueueAdmission(queueName string, id int64, selection queueSelection, policy Policy, holdPolicy HoldPolicy, budgetSource string, pressureTag string) *PriorityAdmission {
  355. score, ok := selection.scores[id]
  356. if !ok {
  357. return nil
  358. }
  359. admission := &PriorityAdmission{
  360. Basis: queueName,
  361. Score: score,
  362. Cutoff: selection.cutoff,
  363. Tier: selection.tiers[id],
  364. }
  365. if _, ok := selection.selected[id]; ok {
  366. if _, held := selection.held[id]; held {
  367. admission.Class = AdmissionClassHold
  368. extras := []string{pressureTag, "pressure:hold", ReasonTagHoldActive, "budget:" + slugToken(budgetSource)}
  369. if _, ok := selection.protected[id]; ok {
  370. extras = append(extras, ReasonTagHoldProtected)
  371. }
  372. admission.Reason = admissionReason("queue:"+queueName+":hold", policy, holdPolicy, extras...)
  373. } else {
  374. admission.Class = AdmissionClassAdmit
  375. extras := []string{pressureTag, "budget:" + slugToken(budgetSource)}
  376. if _, ok := selection.opportunistic[id]; ok {
  377. extras = append(extras, "pressure:hold", ReasonTagDisplaceOpportunist, ReasonTagDisplaceTier, ReasonTagHoldDisplaced)
  378. }
  379. admission.Reason = admissionReason("queue:"+queueName+":admit", policy, holdPolicy, extras...)
  380. }
  381. return admission
  382. }
  383. if _, ok := selection.displaced[id]; ok {
  384. admission.Class = AdmissionClassDisplace
  385. admission.Reason = admissionReason("queue:"+queueName+":displace", policy, holdPolicy, pressureTag, "pressure:hold", ReasonTagDisplaceOpportunist, ReasonTagDisplaceTier, ReasonTagHoldDisplaced, "budget:"+slugToken(budgetSource))
  386. return admission
  387. }
  388. if _, ok := selection.displacedByHold[id]; ok {
  389. admission.Class = AdmissionClassDisplace
  390. admission.Reason = admissionReason("queue:"+queueName+":displace", policy, holdPolicy, pressureTag, "pressure:hold", ReasonTagHoldActive, "budget:"+slugToken(budgetSource))
  391. return admission
  392. }
  393. admission.Class = AdmissionClassDefer
  394. extras := []string{pressureTag, "pressure:budget", "budget:" + slugToken(budgetSource)}
  395. if _, ok := selection.expired[id]; ok {
  396. extras = append(extras, ReasonTagHoldExpired)
  397. }
  398. admission.Reason = admissionReason("queue:"+queueName+":budget", policy, holdPolicy, extras...)
  399. return admission
  400. }
  401. func oldestAge(queue map[int64]*queuedDecision, now time.Time) float64 {
  402. oldest := 0.0
  403. first := true
  404. for _, qd := range queue {
  405. age := now.Sub(qd.FirstSeen).Seconds()
  406. if first || age > oldest {
  407. oldest = age
  408. first = false
  409. }
  410. }
  411. if first {
  412. return 0
  413. }
  414. return oldest
  415. }