Skip to content

Commit 3d0c8f5

Browse files
authored
Merge pull request #9 from doraemonkeys/master
Add CPU Usage Maintenance Feature
2 parents ec1c088 + 797d4f4 commit 3d0c8f5

File tree

9 files changed

+395
-6
lines changed

9 files changed

+395
-6
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Developer will encounter the need to quickly occupy CPU and memory, I am also de
1313
- [ ] macOs
1414
- [ ] Windows
1515
- [x] Memory read/write periodically , prevent memory from being swapped out
16-
- [ ] Dynamic adjustment of CPU and memory usage
16+
- [ ] Dynamic adjustment of memory usage
1717
- [ ] Eat GPU
1818

1919
# Usage
@@ -27,6 +27,7 @@ Usage:
2727

2828
Flags:
2929
--cpu-affinities ints Which cpu core(s) would you want to eat? multiple cores separate by ','
30+
--cpu-maintain string How many cpu would you want maintain(e.g. 50%)
3031
-c, --cpu-usage string How many cpu would you want eat (default "0")
3132
-h, --help help for eat
3233
-r, --memory-refresh-interval string How often to trigger a refresh to prevent the ate memory from being swapped out (default "5m")
@@ -47,6 +48,9 @@ eat -c 3 -m 200m # eating 3 CPU core and 200MB memory
4748
eat -c 100% -m 100% # eating all CPU core and memory
4849
eat -c 100% -t 1h # eating all CPU core and quit after 1hour
4950

51+
eat --cpu-maintain 50% # dynamic adjust to maintain minimum 50% CPU usage
52+
eat --cpu-maintain 50% -c 100% # dynamic adjust to maintain minimum 50% CPU usage and use all CPU core
53+
5054
eat --cpu-affinities 0 -c 1 # only run eat in core #0 (first core)
5155
eat --cpu-affinities 0,1 -c 2 # run eat in core #0,1 (first and second core)
5256
eat --cpu-affinities 0,1,2,3 -c 100% # error case: in-enough cpu affinities
@@ -101,6 +105,7 @@ $ ./eat.out --help
101105

102106
Flags:
103107
--cpu-affinities 整数 指定在几个核心上运行 Eat,多个核心索引之间用 ',' 分隔,索引从 0 开始。
108+
--cpu-maintain 字符串 你想将CPU使用率维持在多少(e.g. 50%)
104109
-c, --cpu-usage 字符串 你想吃掉多少个 CPU(默认为 '0')?
105110
-h,--help 输出 eat 的帮助
106111
-r, --memory-refresh-interval 字符串 每隔多长时间触发一次刷新,以防止被吃掉的内存被交换出去(默认值为 '5m'
@@ -121,6 +126,9 @@ eat -c 3 -m 200m # 占用3个CPU核和200MB内存
121126
eat -c 100% -m 100% # 占用所有CPU核和内存
122127
eat -c 100% -t 1h # 占用所有CPU核并在一小时后退出
123128
129+
eat --cpu-maintain 50% # 动态调整维持50%的CPU使用率
130+
eat --cpu-maintain 50% -c 100% # 使用所有CPU核心动态调整维持50%的CPU使用率
131+
124132
eat --cpu-affinities 0 -c 1 # 只占用 #0 第一个核心
125133
eat --cpu-affinities 0,1 -c 2 # 占用 #0,1 前两个个核心
126134
eat --cpu-affinities 0,1,2,3 -c 100% # 错误参数: 每个请求核都要指定对应的亲和性核心

cmd/cpu.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import (
55
"fmt"
66
"log"
77
"math"
8+
"math/rand/v2"
89
"runtime"
910
"sync"
1011
"time"
1112

1213
"eat/cmd/cpu_affinity"
14+
"eat/cmd/sysinfo"
1315
)
1416

1517
func busyWork(ctx context.Context) {
@@ -142,3 +144,153 @@ func eatCPU(ctx context.Context, wg *sync.WaitGroup, c float64, cpuAffinitiesEat
142144

143145
fmt.Printf("Ate %2.3f CPU cores\n", c)
144146
}
147+
148+
func maintainCpuUsage(ctx context.Context, wg *sync.WaitGroup, coreNum float64, usagePercent float64, cpuAffinitiesEat []uint, cpuMonitor sysinfo.SystemCPUMonitor) {
149+
if coreNum == 0 {
150+
coreNum = float64(runtime.NumCPU())
151+
}
152+
fmt.Printf("CPU usage will be maintaining at minimum %.3f%%, eating %.3f cores, be patient...\n", usagePercent, coreNum)
153+
154+
wg.Add(1)
155+
go func() {
156+
defer wg.Done()
157+
MaintainCpuUsage(ctx, coreNum, usagePercent, cpuAffinitiesEat, cpuMonitor)
158+
}()
159+
}
160+
161+
func MaintainCpuUsage(ctx context.Context, coreNum float64, usagePercent float64, cpuAffinitiesEat []uint, cpuMonitor sysinfo.SystemCPUMonitor) {
162+
runtime.GOMAXPROCS(runtime.NumCPU())
163+
164+
fullCores := int(coreNum)
165+
partialCoreRatio := coreNum - float64(fullCores)
166+
167+
const maxIdleDuration = 1 * time.Second
168+
const minIdleDuration = 1 * time.Millisecond
169+
const initIdleDurationAdjustRatio float64 = 0.1
170+
const minIdleDurationAdjustRatio = 0.002
171+
var idleDuration = maxIdleDuration
172+
var dynIdleDurationAdjustRatio float64 = initIdleDurationAdjustRatio
173+
var stopWork = false
174+
var cur float64 = 0
175+
var ctxDone = false
176+
177+
var fixIdleDuration = func() {
178+
var err error
179+
cur, err = cpuMonitor.GetCPUUsage()
180+
if err != nil {
181+
log.Printf("MaintainCpuUsage: get cpu usage failed, reason: %s", err.Error())
182+
return
183+
}
184+
// When the cpu usage fluctuates greatly, increase idleDurationAdjustRatio to stabilize the cpu usage
185+
if dynIdleDurationAdjustRatio == minIdleDurationAdjustRatio {
186+
if cur > usagePercent+20 || cur < usagePercent-20 {
187+
dynIdleDurationAdjustRatio = initIdleDurationAdjustRatio
188+
} else if cur > usagePercent+10 || cur < usagePercent-10 {
189+
dynIdleDurationAdjustRatio = initIdleDurationAdjustRatio * 0.5
190+
}
191+
}
192+
if cur > usagePercent {
193+
idleDuration = time.Duration(float64(idleDuration) * (1 + dynIdleDurationAdjustRatio))
194+
} else if cur < usagePercent {
195+
idleDuration = time.Duration(float64(idleDuration) * (1 - dynIdleDurationAdjustRatio))
196+
}
197+
if idleDuration < minIdleDuration {
198+
idleDuration = minIdleDuration
199+
} else if idleDuration > maxIdleDuration {
200+
idleDuration = maxIdleDuration
201+
stopWork = true
202+
} else {
203+
stopWork = false
204+
}
205+
// gradually decrease the idle duration adjustment ratio, make the idle duration more stable
206+
dynIdleDurationAdjustRatio -= 0.001
207+
dynIdleDurationAdjustRatio = max(minIdleDurationAdjustRatio, dynIdleDurationAdjustRatio)
208+
}
209+
var worker = func(wg *sync.WaitGroup, idx int, workerName string, work func()) {
210+
defer wg.Done()
211+
cleanup, err := setCpuAffWrapper(idx, cpuAffinitiesEat)
212+
if err != nil {
213+
fmt.Printf("Error: %s failed to set cpu affinities, reason: %s\n", workerName, err.Error())
214+
return
215+
}
216+
if cleanup != nil {
217+
fmt.Printf("Worker %s: CPU affinities set to %d\n", workerName, cpuAffinitiesEat[idx])
218+
defer cleanup()
219+
}
220+
for {
221+
if ctxDone {
222+
return
223+
}
224+
if !stopWork {
225+
work()
226+
}
227+
// if idleDuration is less than 1ms, do not sleep, directly execute fixIdleDuration
228+
if idleDuration > time.Millisecond*1 {
229+
time.Sleep(idleDuration)
230+
}
231+
}
232+
}
233+
cpuIntensiveTask := GenerateCPUIntensiveTask(time.Microsecond * 2000) // 2ms is empirical data
234+
wg := &sync.WaitGroup{}
235+
for i := 0; i < fullCores; i++ {
236+
wg.Add(1)
237+
workerName := fmt.Sprintf("%d@fullCore", i)
238+
go worker(wg, i, workerName, cpuIntensiveTask)
239+
}
240+
if partialCoreRatio > 0 {
241+
wg.Add(1)
242+
workerName := fmt.Sprintf("%d@partCore", fullCores)
243+
go worker(wg, fullCores, workerName, cpuIntensiveTask)
244+
}
245+
fmt.Print("\033[?25l") // hide cursor
246+
defer fmt.Print("\033[?25h") // show cursor
247+
248+
ticker := time.NewTicker(time.Millisecond * 300)
249+
defer ticker.Stop()
250+
for {
251+
select {
252+
case <-ctx.Done():
253+
ctxDone = true
254+
fmt.Println("MaintainCpuUsage: quit due to context being cancelled")
255+
wg.Wait()
256+
return
257+
case <-ticker.C:
258+
fixIdleDuration()
259+
printCpuInfo(cur, idleDuration, dynIdleDurationAdjustRatio)
260+
}
261+
}
262+
}
263+
264+
func printCpuInfo(usagePercent float64, idleDuration time.Duration, ratio float64) {
265+
// clear current line
266+
fmt.Print("\033[2K")
267+
fmt.Printf("Idle: %s\n", idleDuration)
268+
fmt.Printf("Ratio: %.3f%%\n", ratio)
269+
fmt.Printf("CPU usage:\033[32m%6.2f%%\033[0m\n", usagePercent)
270+
fmt.Print("\033[3A\033[G")
271+
}
272+
273+
// GenerateCPUIntensiveTask returns a function that performs a duration CPU-intensive task
274+
func GenerateCPUIntensiveTask(duration time.Duration) func() {
275+
const N = 1000
276+
start := time.Now()
277+
iteration := 0
278+
var cnt int64 = 0
279+
var i int
280+
for time.Since(start) < duration {
281+
for i = 0; i < N; i++ {
282+
cnt = cnt * rand.Int64N(100)
283+
iteration++
284+
}
285+
}
286+
return func() {
287+
var cnt2 int64 = cnt
288+
for i = 0; i < iteration; i++ {
289+
cnt2 = cnt2 * rand.Int64N(100)
290+
cnt++
291+
}
292+
// KeepAlive ensures that the variable is not optimized away by the compiler
293+
runtime.KeepAlive(cnt)
294+
runtime.KeepAlive(cnt2)
295+
}
296+
}

cmd/cpu_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestGenerateCPUIntensiveTask(t *testing.T) {
9+
10+
tests := []struct {
11+
name string
12+
duration time.Duration
13+
}{
14+
{"2ms", time.Millisecond * 2},
15+
{"5ms", time.Millisecond * 5},
16+
{"20ms", time.Millisecond * 20},
17+
{"100ms", time.Millisecond * 100},
18+
{"500ms", time.Millisecond * 500},
19+
{"1s", time.Second},
20+
{"2s", time.Second * 2},
21+
}
22+
for _, tt := range tests {
23+
t.Run(tt.name, func(t *testing.T) {
24+
got := GenerateCPUIntensiveTask(tt.duration)
25+
now := time.Now()
26+
got()
27+
after := time.Now()
28+
if (after.Sub(now).Abs() - tt.duration) > max(tt.duration*8/10, time.Millisecond*2) {
29+
t.Errorf("GenerateCPUIntensiveTask() = %v, want %v", after.Sub(now), max(tt.duration*8/10, time.Millisecond*2))
30+
}
31+
})
32+
}
33+
}

cmd/parse.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package cmd
33
import (
44
"fmt"
55
"math"
6+
"os"
67
"runtime"
78
"strconv"
9+
"strings"
810
"time"
911

1012
"eat/cmd/cpu_affinity"
13+
1114
"github.com/pbnjay/memory"
1215
)
1316

@@ -35,6 +38,28 @@ func parseEatCPUCount(c string) float64 {
3538
}
3639
}
3740

41+
// parseCPUMaintainPercent parse cpu usage percent, return value is percent(0-100)
42+
func parseCPUMaintainPercent(c string) float64 {
43+
if c == "" {
44+
return 0
45+
}
46+
if !strings.HasSuffix(c, "%") {
47+
fmt.Println("Error: invalid cpu maintain percent, must end with %")
48+
os.Exit(1)
49+
}
50+
c = strings.TrimSuffix(c, "%")
51+
cMaintain, err := strconv.ParseFloat(c, 32)
52+
if err != nil {
53+
fmt.Println("Error: invalid cpu maintain percent:", c)
54+
os.Exit(1)
55+
}
56+
if cMaintain < 0 || cMaintain > 100 {
57+
fmt.Println("Error: invalid cpu maintain percent:", cMaintain)
58+
os.Exit(1)
59+
}
60+
return cMaintain
61+
}
62+
3863
func parseEatMemoryBytes(m string) uint64 {
3964
// allow g/G, m/M, k/K suffixes
4065
// 1G = 1024M = 1048576K

cmd/root.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
"syscall"
1414
"time"
1515

16+
"eat/cmd/sysinfo"
1617
"eat/cmd/version"
18+
1719
"github.com/pbnjay/memory"
1820
"github.com/spf13/cobra"
1921
)
@@ -85,17 +87,22 @@ func eatFunction(cmd *cobra.Command, _ []string) {
8587

8688
// Get the flags
8789
c, _ := cmd.Flags().GetString("cpu-usage")
90+
cMaintain, _ := cmd.Flags().GetString("cpu-maintain")
8891
cAff, _ := cmd.Flags().GetIntSlice("cpu-affinities")
8992
m, _ := cmd.Flags().GetString("memory-usage")
9093
dl, _ := cmd.Flags().GetString("time-deadline")
9194
r, _ := cmd.Flags().GetString("memory-refresh-interval")
9295

93-
if c == "0" && m == "0m" {
96+
if c == "0" && m == "0m" && cMaintain == "" {
9497
fmt.Println("Error: no cpu or memory usage specified")
9598
return
9699
}
100+
if c == "0" && cMaintain != "" {
101+
c = cMaintain
102+
}
97103

98104
cEat := parseEatCPUCount(c)
105+
cMaintainPercent := parseCPUMaintainPercent(cMaintain)
99106
phyCores := runtime.NumCPU()
100107
if int(math.Ceil(cEat)) > phyCores {
101108
fmt.Printf("Error: user specified cpu cores exceed system physical cores(%d)\n", phyCores)
@@ -115,7 +122,11 @@ func eatFunction(cmd *cobra.Command, _ []string) {
115122
defer cancel()
116123
fmt.Printf("Want to eat %2.3fCPU, %s Memory\n", cEat, m)
117124
eatMemory(rootCtx, &wg, mEat, mAteRenew)
118-
eatCPU(rootCtx, &wg, cEat, cpuAffinitiesEat)
125+
if cMaintainPercent > 0 {
126+
maintainCpuUsage(rootCtx, &wg, cEat, cMaintainPercent, cpuAffinitiesEat, sysinfo.Monitor)
127+
} else {
128+
eatCPU(rootCtx, &wg, cEat, cpuAffinitiesEat)
129+
}
119130
// in case that all sub goroutines are dead due to runtime error like memory not enough.
120131
// so the main goroutine automatically quit as well, don't wait user ctrl+c or context deadline.
121132
go func(wgp *sync.WaitGroup) {

0 commit comments

Comments
 (0)