Skip to content

Commit 3bce429

Browse files
authored
Add per-process disk I/O read and write speed to process management (#1179)
* Update dartssh2 * Add process disk IO read/write rates * Add issuers of #1175 and #1178 to participants list * Fix globbing in process script and extract state helpers
1 parent e52ef43 commit 3bce429

6 files changed

Lines changed: 406 additions & 24 deletions

File tree

lib/data/model/app/scripts/script_builders.dart

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ switch (\$args[0]) {
111111
disabledCmdTypes: disabledCmdTypes ?? [],
112112
),
113113
ShellFunc.process =>
114-
'Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet | ConvertTo-Json',
114+
r'''
115+
Get-Process | Select-Object ProcessName, Id, CPU, WorkingSet,
116+
@{Name='IOReadBytes';Expression={$_.IOReadBytes}},
117+
@{Name='IOWriteBytes';Expression={$_.IOWriteBytes}} | ConvertTo-Json''',
115118
ShellFunc.shutdown => 'Stop-Computer -Force',
116119
ShellFunc.reboot => 'Restart-Computer -Force',
117120
ShellFunc.suspend =>
@@ -253,7 +256,22 @@ if [ "\$macSign" = "" ] && [ "\$bsdSign" = "" ]; then
253256
\tif [ "\$isBusybox" != "" ]; then
254257
\t\tps w
255258
\telse
256-
\t\tps -aux
259+
\t\tprintf 'PID USER %%CPU %%MEM VSZ RSS TTY STAT START TIME READ_BYTES WRITE_BYTES COMMAND\\n'
260+
\t\tps -axo pid=,user=,%cpu=,%mem=,vsz=,rss=,tty=,stat=,start=,time=,args= | while IFS= read -r line; do
261+
\t\t\tset -f
262+
\t\t\tset -- \$line
263+
\t\t\tset +f
264+
\t\t\tpid=\$1; user=\$2; cpu=\$3; mem=\$4; vsz=\$5; rss=\$6; tty=\$7; stat=\$8; start=\$9; time=\${10}
265+
\t\t\tshift 10
266+
\t\t\tcmd=\$*
267+
\t\t\tread_bytes='-'
268+
\t\t\twrite_bytes='-'
269+
\t\t\tif [ -r "/proc/\$pid/io" ]; then
270+
\t\t\t\tread_bytes=\$(awk '/^read_bytes:/ {print \$2}' "/proc/\$pid/io")
271+
\t\t\t\twrite_bytes=\$(awk '/^write_bytes:/ {print \$2}' "/proc/\$pid/io")
272+
\t\t\tfi
273+
\t\t\tprintf '%s %s %s %s %s %s %s %s %s %s %s %s %s\\n' "\$pid" "\$user" "\$cpu" "\$mem" "\$vsz" "\$rss" "\$tty" "\$stat" "\$start" "\$time" "\$read_bytes" "\$write_bytes" "\$cmd"
274+
\t\tdone
257275
\tfi
258276
else
259277
\tps -ax

lib/data/model/server/proc.dart

Lines changed: 230 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:fl_lib/fl_lib.dart';
1+
import 'dart:convert';
22

33
import 'package:server_box/data/res/misc.dart';
44

@@ -13,6 +13,8 @@ class _ProcValIdxMap {
1313
final int? stat;
1414
final int? start;
1515
final int? time;
16+
final int? readBytes;
17+
final int? writeBytes;
1618
final int command;
1719

1820
const _ProcValIdxMap({
@@ -26,6 +28,8 @@ class _ProcValIdxMap {
2628
this.stat,
2729
this.start,
2830
this.time,
31+
this.readBytes,
32+
this.writeBytes,
2933
required this.command,
3034
});
3135
}
@@ -42,6 +46,10 @@ class Proc {
4246
final String? stat;
4347
final String? start;
4448
final String? time;
49+
final int? readBytes;
50+
final int? writeBytes;
51+
final double? readSpeed;
52+
final double? writeSpeed;
4553
final String command;
4654

4755
const Proc({
@@ -55,11 +63,28 @@ class Proc {
5563
this.stat,
5664
this.start,
5765
this.time,
66+
this.readBytes,
67+
this.writeBytes,
68+
this.readSpeed,
69+
this.writeSpeed,
5870
required this.command,
5971
});
6072

61-
factory Proc._parse(String raw, _ProcValIdxMap map) {
73+
factory Proc._parse(
74+
String raw,
75+
_ProcValIdxMap map, {
76+
Proc? previous,
77+
double? elapsedSeconds,
78+
}) {
6279
final parts = raw.split(RegExp(r'\s+'));
80+
final readBytes = _parseNullableInt(parts, map.readBytes);
81+
final writeBytes = _parseNullableInt(parts, map.writeBytes);
82+
final (readSpeed, writeSpeed) = _calculateSpeeds(
83+
readBytes: readBytes,
84+
writeBytes: writeBytes,
85+
previous: previous,
86+
elapsedSeconds: elapsedSeconds,
87+
);
6388
return Proc(
6489
user: map.user == null ? null : parts[map.user!],
6590
pid: int.parse(parts[map.pid]),
@@ -71,10 +96,47 @@ class Proc {
7196
stat: map.stat == null ? null : parts[map.stat!],
7297
start: map.start == null ? null : parts[map.start!],
7398
time: map.time == null ? null : parts[map.time!],
99+
readBytes: readBytes,
100+
writeBytes: writeBytes,
101+
readSpeed: readSpeed,
102+
writeSpeed: writeSpeed,
74103
command: parts.sublist(map.command).join(' '),
75104
);
76105
}
77106

107+
factory Proc._parseWindowsJson(
108+
Map<String, dynamic> raw, {
109+
Proc? previous,
110+
double? elapsedSeconds,
111+
}) {
112+
final readBytes = _parseDynamicInt(
113+
raw['IOReadBytes'] ?? raw['ReadTransferCount'],
114+
);
115+
final writeBytes = _parseDynamicInt(
116+
raw['IOWriteBytes'] ?? raw['WriteTransferCount'],
117+
);
118+
final (readSpeed, writeSpeed) = _calculateSpeeds(
119+
readBytes: readBytes,
120+
writeBytes: writeBytes,
121+
previous: previous,
122+
elapsedSeconds: elapsedSeconds,
123+
);
124+
final name = raw['ProcessName'] ?? raw['Name'];
125+
final command = raw['CommandLine'] ?? raw['Path'] ?? name ?? '';
126+
return Proc(
127+
pid: _parseDynamicInt(raw['Id'] ?? raw['ProcessId'])!,
128+
cpu: _parseDynamicDouble(raw['CPU']),
129+
rss: _parseDynamicInt(
130+
raw['WorkingSet'] ?? raw['WorkingSetSize'],
131+
)?.toString(),
132+
readBytes: readBytes,
133+
writeBytes: writeBytes,
134+
readSpeed: readSpeed,
135+
writeSpeed: writeSpeed,
136+
command: command.toString(),
137+
);
138+
}
139+
78140
Map toJson() {
79141
return {
80142
'user': user,
@@ -87,6 +149,10 @@ class Proc {
87149
'stat': stat,
88150
'start': start,
89151
'time': time,
152+
'readBytes': readBytes,
153+
'writeBytes': writeBytes,
154+
'readSpeed': readSpeed,
155+
'writeSpeed': writeSpeed,
90156
'command': command,
91157
};
92158
}
@@ -106,12 +172,41 @@ class Proc {
106172
class PsResult {
107173
final List<Proc> procs;
108174
final String? error;
175+
final int sampledAtMillis;
176+
177+
const PsResult({required this.procs, this.error, this.sampledAtMillis = 0});
109178

110-
const PsResult({required this.procs, this.error});
179+
factory PsResult.parse(
180+
String raw, {
181+
ProcSortMode sort = ProcSortMode.cpu,
182+
PsResult? previous,
183+
int? sampledAtMillis,
184+
}) {
185+
final currentSampledAtMillis =
186+
sampledAtMillis ?? DateTime.now().millisecondsSinceEpoch;
187+
final previousByPid = {
188+
for (final proc in previous?.procs ?? const <Proc>[]) proc.pid: proc,
189+
};
190+
final elapsedSeconds = previous == null || previous.sampledAtMillis <= 0
191+
? null
192+
: (currentSampledAtMillis - previous.sampledAtMillis) / 1000.0;
193+
final jsonResult = _parseWindowsJsonResult(
194+
raw,
195+
previousByPid: previousByPid,
196+
elapsedSeconds: elapsedSeconds,
197+
sampledAtMillis: currentSampledAtMillis,
198+
sort: sort,
199+
);
200+
if (jsonResult != null) return jsonResult;
111201

112-
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
113-
final lines = raw.split('\n').map((e) => e.trim()).toList();
114-
if (lines.isEmpty) return const PsResult(procs: [], error: null);
202+
final lines = raw
203+
.split('\n')
204+
.map((e) => e.trim())
205+
.where((e) => e.isNotEmpty)
206+
.toList();
207+
if (lines.isEmpty) {
208+
return PsResult(procs: const [], sampledAtMillis: currentSampledAtMillis);
209+
}
115210

116211
final header = lines[0];
117212
final parts = header.split(RegExp(r'\s+'));
@@ -127,6 +222,8 @@ class PsResult {
127222
stat: parts.indexOfOrNull('STAT'),
128223
start: parts.indexOfOrNull('START'),
129224
time: parts.indexOfOrNull('TIME'),
225+
readBytes: parts.indexOfOrNull('READ_BYTES'),
226+
writeBytes: parts.indexOfOrNull('WRITE_BYTES'),
130227
command: parts.indexOfOrNull('COMMAND') ?? parts.indexOfOrNull('CMD')!,
131228
);
132229

@@ -136,19 +233,83 @@ class PsResult {
136233
final line = lines[i];
137234
if (line.isEmpty) continue;
138235
try {
139-
procs.add(Proc._parse(line, map));
140-
} catch (e, trace) {
236+
final pid = _parsePid(line, map.pid);
237+
procs.add(
238+
Proc._parse(
239+
line,
240+
map,
241+
previous: previousByPid[pid],
242+
elapsedSeconds: elapsedSeconds,
243+
),
244+
);
245+
} catch (e) {
141246
errs.add('$line: $e');
142-
Loggers.app.warning('Process failed', e, trace);
143247
}
144248
}
145249

250+
_sort(procs, sort);
251+
return PsResult(
252+
procs: procs,
253+
error: errs.isEmpty ? null : errs.join('\n'),
254+
sampledAtMillis: currentSampledAtMillis,
255+
);
256+
}
257+
258+
static PsResult? _parseWindowsJsonResult(
259+
String raw, {
260+
required Map<int, Proc> previousByPid,
261+
required double? elapsedSeconds,
262+
required int sampledAtMillis,
263+
required ProcSortMode sort,
264+
}) {
265+
final trimmed = raw.trim();
266+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null;
267+
try {
268+
final decoded = json.decode(trimmed);
269+
final items = decoded is List ? decoded : [decoded];
270+
final procs = <Proc>[];
271+
final errs = <String>[];
272+
for (final item in items) {
273+
if (item is! Map) continue;
274+
try {
275+
final map = Map<String, dynamic>.from(item);
276+
final pid = _parseDynamicInt(map['Id'] ?? map['ProcessId']);
277+
if (pid == null) continue;
278+
procs.add(
279+
Proc._parseWindowsJson(
280+
map,
281+
previous: previousByPid[pid],
282+
elapsedSeconds: elapsedSeconds,
283+
),
284+
);
285+
} catch (e) {
286+
errs.add('$item: $e');
287+
}
288+
}
289+
_sort(procs, sort);
290+
return PsResult(
291+
procs: procs,
292+
error: errs.isEmpty ? null : errs.join('\n'),
293+
sampledAtMillis: sampledAtMillis,
294+
);
295+
} catch (_) {
296+
return null;
297+
}
298+
}
299+
300+
static void _sort(List<Proc> procs, ProcSortMode sort) {
146301
switch (sort) {
147302
case ProcSortMode.cpu:
148-
procs.sort((a, b) => b.cpu?.compareTo(a.cpu ?? 0) ?? 0);
303+
procs.sort((a, b) => _compareNullableDesc(a.cpu, b.cpu));
149304
break;
150305
case ProcSortMode.mem:
151-
procs.sort((a, b) => b.mem?.compareTo(a.mem ?? 0) ?? 0);
306+
procs.sort((a, b) => _compareNullableDesc(a.mem, b.mem));
307+
break;
308+
case ProcSortMode.read:
309+
procs.sort((a, b) => _compareNullableDesc(a.readSpeed, b.readSpeed));
310+
break;
311+
case ProcSortMode.write:
312+
procs.sort((a, b) => _compareNullableDesc(a.writeSpeed, b.writeSpeed));
152313
break;
153314
case ProcSortMode.pid:
154315
procs.sort((a, b) => a.pid.compareTo(b.pid));
@@ -160,15 +321,71 @@ class PsResult {
160321
procs.sort((a, b) => a.binary.compareTo(b.binary));
161322
break;
162323
}
163-
return PsResult(procs: procs, error: errs.isEmpty ? null : errs.join('\n'));
164324
}
165325
}
166326

167-
enum ProcSortMode { cpu, mem, pid, user, name }
327+
enum ProcSortMode { cpu, mem, read, write, pid, user, name }
168328

169329
extension _StrIndex on List<String> {
170330
int? indexOfOrNull(String val) {
171331
final idx = indexOf(val);
172332
return idx == -1 ? null : idx;
173333
}
174334
}
335+
336+
int _parsePid(String raw, int pidIndex) {
337+
final parts = raw.split(RegExp(r'\s+'));
338+
return int.parse(parts[pidIndex]);
339+
}
340+
341+
int? _parseNullableInt(List<String> parts, int? idx) {
342+
if (idx == null || idx >= parts.length) return null;
343+
return _parseDynamicInt(parts[idx]);
344+
}
345+
346+
int? _parseDynamicInt(Object? val) {
347+
if (val == null) return null;
348+
if (val is int) return val;
349+
if (val is num) return val.toInt();
350+
final str = val.toString();
351+
if (str.isEmpty || str == '-') return null;
352+
return int.tryParse(str);
353+
}
354+
355+
double? _parseDynamicDouble(Object? val) {
356+
if (val == null) return null;
357+
if (val is double) return val;
358+
if (val is num) return val.toDouble();
359+
final str = val.toString();
360+
if (str.isEmpty || str == '-') return null;
361+
return double.tryParse(str);
362+
}
363+
364+
(double?, double?) _calculateSpeeds({
365+
required int? readBytes,
366+
required int? writeBytes,
367+
required Proc? previous,
368+
required double? elapsedSeconds,
369+
}) {
370+
if (previous == null || elapsedSeconds == null || elapsedSeconds <= 0) {
371+
return (null, null);
372+
}
373+
return (
374+
_calculateSpeed(readBytes, previous.readBytes, elapsedSeconds),
375+
_calculateSpeed(writeBytes, previous.writeBytes, elapsedSeconds),
376+
);
377+
}
378+
379+
double? _calculateSpeed(int? current, int? previous, double elapsedSeconds) {
380+
if (current == null || previous == null) return null;
381+
final diff = current - previous;
382+
if (diff < 0) return null;
383+
return diff / elapsedSeconds;
384+
}
385+
386+
int _compareNullableDesc(num? a, num? b) {
387+
if (a == null && b == null) return 0;
388+
if (a == null) return 1;
389+
if (b == null) return -1;
390+
return b.compareTo(a);
391+
}

lib/data/res/github_id.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ abstract final class GithubIds {
175175
'kuilei0926',
176176
'ci-sourcerer',
177177
'misaki258',
178+
'Muska-Ami',
179+
'wu4339'
178180
};
179181
}
180182

0 commit comments

Comments
 (0)