querysub 0.700.0 → 0.702.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.700.0",
3
+ "version": "0.702.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -41,6 +41,8 @@ function createSourceWindows(
41
41
 
42
42
  function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
43
43
  const ONTARIO = `https://99-250-124-91.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
44
+ // NOTE: This is on an overly large extra disk that's on our 4090 machine. I prefer to not use it because it might slow down diffusion, but it's quite large (2tb, and diffusion will never use more than 1tb), so we could use it if we needed to.
45
+ const ONTARIO2 = `https://99-250-124-91.querysubtest.com:5235/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
44
46
  const HETZNER = `https://65-109-93-113.querysubtest.com:5234/file/${STORAGE_ACCOUNT}/${bucket}/storage/storagerouting.json`;
45
47
  const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
46
48
 
@@ -76,6 +78,12 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
76
78
  // NOTE: Once we add new sources, we need to add something at a new start time in the future so we have time to move the files.
77
79
  // TODO: EVENTUALLY, Once we have enough data, instead of restriping, we need to bisect the data. That way we can slowly move over the data instead of trying to rewrite all of our data all at once. However, for the moment, moving all the data is probably fine. On the data center, we should be able to move terabytes of data every hour, so we should be able to handle quite a bit, especially if we have a few days to run the restriping.
78
80
  // IMPORTANT! Every time we re-stripe, we need to increment the route version. Otherwise, we could reuse a folder that we previously used that's no longer in sync. which will cause us to resurrect old data, which is very bad. We don't want to do that...
81
+ // NOTE: We do full redundancy, so if the ranges are a, b, c, then its:
82
+ // s0=a s1=b s2=c
83
+ // s0=c s1=a s2=b
84
+ // s0=b s1=c s2=a
85
+ // Essentially, we just shift it every time. So every server has all the data, But the primary writers are divided between all the servers.
86
+ // (If you follow A, you can see it's diagonal, B is diagonal, they're all diagonal, like stripes).
79
87
  ...stripeSources([ONTARIO, HETZNER], { routeVersion: 2 }),
80
88
  { type: "backblaze", url: BACKBLAZE, name: "backblaze" },
81
89
  ]
@@ -83,7 +91,7 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
83
91
  ]);
84
92
 
85
93
  return createArchives({
86
- version: 28,
94
+ version: 29,
87
95
  sources,
88
96
  });
89
97
  }
@@ -134,13 +134,21 @@ export class MachineDetailPage extends qreact.Component {
134
134
  <ShowMore className={css.whiteSpace("pre-wrap")} maxHeight={80}>
135
135
  {(() => {
136
136
  if (typeof value === "object") {
137
- return <UsageBar label={value.label || value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
137
+ return <UsageBar label={value.label || value.type} value={value.value} max={value.max} subValue={value.subValue} subLabel={value.subLabel} {...getUsageThresholds(value.type)} />;
138
138
  }
139
139
  return value;
140
140
  })()}
141
141
  </ShowMore>
142
142
  </div>
143
143
  ))}
144
+ {machine.meminfo && (
145
+ <div className={css.hbox(10).hsla(0, 0, 0, 0.1).pad2(10, 2)}>
146
+ <b className={css.flexShrink0}>meminfo:</b>
147
+ <ShowMore className={css.whiteSpace("pre-wrap")} maxHeight={80}>
148
+ {Object.entries(machine.meminfo).map(([key, bytes]) => `${key}: ${formatNumber(bytes)}B`).join("\n")}
149
+ </ShowMore>
150
+ </div>
151
+ )}
144
152
  </div>
145
153
  </div>
146
154
 
@@ -187,7 +187,7 @@ export class MachinesListPage extends qreact.Component {
187
187
  <RenderGitRefInfo gitRef={machineInfo.gitRef} />
188
188
  {Object.values(machineInfo.info).map(value => {
189
189
  if (typeof value === "string") return undefined;
190
- return <UsageBar label={value.label || value.type} value={value.value} max={value.max} {...getUsageThresholds(value.type)} />;
190
+ return <UsageBar label={value.label || value.type} value={value.value} max={value.max} subValue={value.subValue} subLabel={value.subLabel} {...getUsageThresholds(value.type)} />;
191
191
  })}
192
192
  </div>
193
193
  <div className={css.vbox(4).flexGrow(1)}>
@@ -63,6 +63,45 @@ const getMemoryInfo = measureWrap(async function getMemoryInfo(): Promise<{ memo
63
63
  });
64
64
 
65
65
 
66
+ const MEMINFO_LINE_REGEX = /^(\S+):\s+(\d+)(?:\s+(\S+))?\s*$/;
67
+ const MEMINFO_UNIT_MULTIPLIERS: Record<string, number> = {
68
+ b: 1,
69
+ kb: 1024,
70
+ mb: 1024 * 1024,
71
+ gb: 1024 * 1024 * 1024,
72
+ };
73
+
74
+ export function parseMeminfo(raw: string): Record<string, number> {
75
+ let result: Record<string, number> = {};
76
+ for (let line of raw.split("\n")) {
77
+ let match = MEMINFO_LINE_REGEX.exec(line);
78
+ if (!match) continue;
79
+ let [, key, valueText, unit] = match;
80
+ let multiplier = unit ? MEMINFO_UNIT_MULTIPLIERS[unit.toLowerCase()] : 1;
81
+ if (multiplier === undefined) continue;
82
+ let value = parseInt(valueText);
83
+ if (!(value >= 0)) continue;
84
+ result[key] = value * multiplier;
85
+ }
86
+ return result;
87
+ }
88
+
89
+ const getMeminfo = measureWrap(async function getMeminfo(): Promise<Record<string, number> | undefined> {
90
+ if (os.platform() === "win32") {
91
+ throw new Error("Windows is not supported for machine resource monitoring");
92
+ }
93
+ try {
94
+ let raw = await fs.promises.readFile("/proc/meminfo", "utf8");
95
+ let parsed = parseMeminfo(raw);
96
+ if (Object.keys(parsed).length > 0) {
97
+ return parsed;
98
+ }
99
+ } catch (e: any) {
100
+ console.warn(`Error reading /proc/meminfo: ${e.stack}`);
101
+ }
102
+ return undefined;
103
+ });
104
+
66
105
  const LARGE_DISK_MIN_BYTES = 100 * 1024 * 1024 * 1024;
67
106
  const DF_LINE_REGEX = /^(\S+)\s+(\d+)\s+(\d+)\s+\d+\s+\S+\s+(.+?)\s*$/;
68
107
 
@@ -93,12 +132,12 @@ const getDiskInfos = measureWrap(async function getDiskInfos(): Promise<DiskInfo
93
132
  }
94
133
  disks.push({ device, mountPoint, value, max });
95
134
  }
135
+ sort(disks, disk => -disk.max);
96
136
  let large = disks.filter(disk => disk.max >= LARGE_DISK_MIN_BYTES);
97
- if (large.length > 0) {
98
- disks = large;
137
+ if (large.length === 0) {
138
+ large = disks.slice(0, 1);
99
139
  }
100
- sort(disks, disk => -disk.max);
101
- return disks;
140
+ return large;
102
141
  } catch (e: any) {
103
142
  console.warn(`Error getting disk info: ${e.stack}`);
104
143
  }
@@ -117,9 +156,10 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
117
156
  };
118
157
 
119
158
  // Get system resource information
120
- let [memoryInfo, diskInfos] = await Promise.all([
159
+ let [memoryInfo, diskInfos, meminfo] = await Promise.all([
121
160
  getMemoryInfo(),
122
- getDiskInfos()
161
+ getDiskInfos(),
162
+ getMeminfo(),
123
163
  ]);
124
164
 
125
165
  if (memoryInfo?.memory) {
@@ -127,9 +167,15 @@ const getLiveMachineInfo = measureWrap(async function getLiveMachineInfo() {
127
167
  type: "MEMORY",
128
168
  value: memoryInfo.memory.value,
129
169
  max: memoryInfo.memory.max,
170
+ subValue: meminfo?.Active,
171
+ subLabel: "active",
130
172
  };
131
173
  }
132
174
 
175
+ if (meminfo) {
176
+ machineInfo.meminfo = meminfo;
177
+ }
178
+
133
179
  if (memoryInfo?.swap) {
134
180
  machineInfo.info.swap = {
135
181
  type: "SWAP",
@@ -64,8 +64,12 @@ export type MachineInfo = {
64
64
  value: number;
65
65
  max: number;
66
66
  label?: string;
67
+ subValue?: number;
68
+ subLabel?: string;
67
69
  }>;
68
70
 
71
+ meminfo?: Record<string, number>;
72
+
69
73
  repoUrl: string;
70
74
  gitRef: string;
71
75
 
@@ -28,12 +28,15 @@ export type InputLabelProps = Omit<InputProps, "label" | "title"> & {
28
28
  tooltip?: string;
29
29
 
30
30
  fillWidth?: boolean | number;
31
+ // Height a textarea opens at in edit mode; EDIT_TEXTAREA_MIN_HEIGHT when unset.
32
+ editMinHeight?: number;
31
33
  };
32
34
 
33
35
  // A checkbox is a small square that reads as belonging to the words beside it, so it sits tight against
34
36
  // its label; every other input is a box of its own and wants room between the two.
35
37
  const LABEL_GAP = 8;
36
38
  const CHECKBOX_LABEL_GAP = 2;
39
+ export const EDIT_TEXTAREA_MIN_HEIGHT = 80;
37
40
 
38
41
  function roundToDecimals(value: number, decimals: number) {
39
42
  return Math.round(value * 10 ** decimals) / 10 ** decimals;
@@ -134,6 +137,10 @@ export class InputLabel extends qreact.Component<InputLabelProps> {
134
137
  };
135
138
  // Default focus on mount, as editting it is the only reason to click on it.
136
139
  props.focusOnMount = props.focusOnMount ?? true;
140
+ if (props.textarea) {
141
+ props.fillWidth = props.fillWidth ?? true;
142
+ style.minHeight = style.minHeight ?? props.editMinHeight ?? EDIT_TEXTAREA_MIN_HEIGHT;
143
+ }
137
144
  }
138
145
  let input = <Input
139
146
  {...props as any}
@@ -141,8 +148,8 @@ export class InputLabel extends qreact.Component<InputLabelProps> {
141
148
  style={style}
142
149
  />;
143
150
  if (props.edit && !this.state.editting) {
144
- input = <span class={css.hbox(2).overflowHidden + " trigger-hover"}>
145
- <span class={props.editClass}>
151
+ input = <span class={css.hbox(2).overflowHidden + (props.textarea && css.fillWidth.alignItems("start") || "") + " trigger-hover"}>
152
+ <span class={(props.textarea && css.whiteSpace("pre-wrap").fillWidth || "") + " " + props.editClass}>
146
153
  {props.editValue ?? props.value}
147
154
  </span>
148
155
  <span class={css.opacity(0.1).opacity(1, "hover")}>
@@ -3,6 +3,8 @@ import { css } from "typesafecss";
3
3
  import { formatNumber } from "socket-function/src/formatting/format";
4
4
 
5
5
  const FLASH_CLASS_NAME = "UsageBar-flash";
6
+ const SUB_BAR_HEIGHT_PERCENT = 35;
7
+ const SUB_BAR_ALPHA = 0.3;
6
8
 
7
9
  export const MEMORY_WARNING_THRESHOLD = 0.7;
8
10
  export const MEMORY_ERROR_THRESHOLD = 0.85;
@@ -36,11 +38,14 @@ export class UsageBar extends qreact.Component<{
36
38
  warningThreshold?: number;
37
39
  /** Fraction of max at which the bar fill turns red and flashes. */
38
40
  errorThreshold?: number;
41
+ subValue?: number;
42
+ subLabel?: string;
39
43
  }> {
40
44
  render() {
41
- let { label, value, max, unit, warningThreshold, errorThreshold } = this.props;
45
+ let { label, value, max, unit, warningThreshold, errorThreshold, subValue, subLabel } = this.props;
42
46
  let suffix = unit || "";
43
47
  let fraction = max && value / max || 0;
48
+ let subFraction = subValue !== undefined && max && subValue / max || 0;
44
49
  let isError = errorThreshold !== undefined && fraction >= errorThreshold;
45
50
  let isWarning = !isError && warningThreshold !== undefined && fraction >= warningThreshold;
46
51
  let fillClass = isError && css.hsl(0, 80, 60) || isWarning && css.hsl(45, 90, 60) || css.hsl(0, 0, 70);
@@ -50,8 +55,12 @@ export class UsageBar extends qreact.Component<{
50
55
  + fillClass
51
56
  + (isError && (" " + FLASH_CLASS_NAME) || "")
52
57
  } />
58
+ {subValue !== undefined && <div className={
59
+ css.absolute.pos(0, `${100 - SUB_BAR_HEIGHT_PERCENT}%`).size(`${Math.min(subFraction, 1) * 100}%`, `${SUB_BAR_HEIGHT_PERCENT}%`).hsla(0, 0, 0, SUB_BAR_ALPHA)
60
+ } />}
53
61
  <div className={css.relative}>
54
62
  {label} ({formatNumber(value)}{suffix} / {formatNumber(max)}{suffix})
63
+ {subValue !== undefined && <span className={css.opacity(0.75)}>, {subLabel || "sub"} {formatNumber(subValue)}{suffix}</span>}
55
64
  </div>
56
65
  {isError && <style>{`
57
66
  @keyframes ${FLASH_CLASS_NAME}-anim {