master
go 167 lines 3.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package intelgpu
4
5 import (
6 "bufio"
7 "bytes"
8 "errors"
9 "os/exec"
10 "strconv"
11 "sync"
12 "time"
13
14 "github.com/netdata/netdata/go/plugins/logger"
15 )
16
17 type intelGpuTop interface {
18 queryGPUSummaryJson() ([]byte, error)
19 stop() error
20 }
21
22 func newIntelGpuTopExec(log *logger.Logger, ndsudoPath string, updateEvery int, device string) (*intelGpuTopExec, error) {
23 topExec := &intelGpuTopExec{
24 Logger: log,
25 ndsudoPath: ndsudoPath,
26 updateEvery: updateEvery,
27 device: device,
28 firstSampleTimeout: time.Second * 3,
29 }
30
31 if err := topExec.run(); err != nil {
32 return nil, err
33 }
34
35 return topExec, nil
36 }
37
38 type intelGpuTopExec struct {
39 *logger.Logger
40
41 ndsudoPath string
42 updateEvery int
43 device string
44 firstSampleTimeout time.Duration
45
46 cmd *exec.Cmd
47 done chan struct{}
48
49 mux sync.Mutex
50 lastSample string
51 }
52
53 func (e *intelGpuTopExec) run() error {
54 var cmd *exec.Cmd
55
56 if e.device != "" {
57 cmd = exec.Command(e.ndsudoPath, "igt-device-json", "--interval", e.calcIntervalArg(), "--device", e.device)
58 } else {
59 cmd = exec.Command(e.ndsudoPath, "igt-json", "--interval", e.calcIntervalArg())
60 }
61
62 e.Debugf("executing '%s'", cmd)
63
64 r, err := cmd.StdoutPipe()
65 if err != nil {
66 return err
67 }
68
69 if err := cmd.Start(); err != nil {
70 return err
71 }
72
73 firstSample := make(chan struct{}, 1)
74 done := make(chan struct{})
75 e.cmd = cmd
76 e.done = done
77
78 go func() {
79 defer close(done)
80 sc := bufio.NewScanner(r)
81 var buf bytes.Buffer
82 var n int
83
84 for sc.Scan() {
85 if n++; n > 1000 {
86 break
87 }
88
89 text := sc.Text()
90
91 if buf.Len() == 0 && text != "{" || text == "" {
92 continue
93 }
94
95 if text == "}," {
96 text = "}"
97 }
98
99 buf.WriteString(text + "\n")
100
101 if text[0] == '}' {
102 e.mux.Lock()
103 e.lastSample = buf.String()
104 e.mux.Unlock()
105
106 select {
107 case firstSample <- struct{}{}:
108 default:
109 }
110
111 buf.Reset()
112 n = 0
113 }
114 }
115 }()
116
117 select {
118 case <-e.done:
119 _ = e.stop()
120 return errors.New("process exited before the first sample was collected")
121 case <-time.After(e.firstSampleTimeout):
122 _ = e.stop()
123 return errors.New("timed out waiting for first sample")
124 case <-firstSample:
125 return nil
126 }
127 }
128
129 func (e *intelGpuTopExec) queryGPUSummaryJson() ([]byte, error) {
130 select {
131 case <-e.done:
132 return nil, errors.New("process has already exited")
133 default:
134 }
135
136 e.mux.Lock()
137 defer e.mux.Unlock()
138
139 return []byte(e.lastSample), nil
140 }
141
142 func (e *intelGpuTopExec) stop() error {
143 if e.cmd == nil || e.cmd.Process == nil {
144 return nil
145 }
146
147 _ = e.cmd.Process.Kill()
148 _ = e.cmd.Wait()
149 e.cmd = nil
150
151 select {
152 case <-e.done:
153 return nil
154 case <-time.After(time.Second * 2):
155 return errors.New("timed out waiting for process to exit")
156 }
157 }
158
159 func (e *intelGpuTopExec) calcIntervalArg() string {
160 // intel_gpu_top appends the end marker ("},\n") of the previous sample to the beginning of the next sample.
161 // interval must be < than 'firstSampleTimeout'
162 interval := 900
163 if m := min(e.updateEvery, int(e.firstSampleTimeout.Seconds())); m > 1 {
164 interval = m*1000 - 500 // milliseconds
165 }
166 return strconv.Itoa(interval)
167 }