Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 07e15aa

Browse files
committedMar 14, 2025·
old page
1 parent cf5b357 commit 07e15aa

File tree

3 files changed

+355
-0
lines changed

3 files changed

+355
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
query ResourceUtilizationForApp(
2+
$team: Slug!
3+
$app: String!
4+
$env: String!
5+
$start: Time!
6+
$end: Time!
7+
) {
8+
team(slug: $team) {
9+
environment(name: $env) {
10+
application(name: $app) {
11+
utilization {
12+
current_cpu: current(resourceType: CPU)
13+
current_memory: current(resourceType: MEMORY)
14+
requested_cpu: requested(resourceType: CPU)
15+
requested_memory: requested(resourceType: MEMORY)
16+
limit_cpu: limit(resourceType: CPU)
17+
limit_memory: limit(resourceType: MEMORY)
18+
cpu_series: series(input: { start: $start, end: $end, resourceType: CPU }) {
19+
timestamp
20+
value
21+
}
22+
memory_series: series(input: { start: $start, end: $end, resourceType: MEMORY }) {
23+
timestamp
24+
value
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
<script lang="ts">
2+
import { page } from '$app/state';
3+
import EChart from '$lib/chart/EChart.svelte';
4+
import GraphErrors from '$lib/GraphErrors.svelte';
5+
import type { EChartsOption } from 'echarts';
6+
7+
import { UtilizationResourceType } from '$houdini';
8+
import { euroValueFormatter } from '$lib/chart/cost_transformer';
9+
import { cpuUtilization, memoryUtilization, yearlyOverageCost } from '$lib/utils/resources';
10+
import { changeParams } from '$lib/utils/searchparams';
11+
import {
12+
BodyLong,
13+
Heading,
14+
Loader,
15+
ToggleGroup,
16+
ToggleGroupItem
17+
} from '@nais/ds-svelte-community';
18+
import { format } from 'date-fns';
19+
import type { CallbackDataParams } from 'echarts/types/dist/shared';
20+
import prettyBytes from 'pretty-bytes';
21+
import type { PageProps } from './$houdini';
22+
23+
let { data }: PageProps = $props();
24+
let { ResourceUtilizationForApp } = $derived(data);
25+
26+
type resourceUsage = {
27+
readonly timestamp: Date;
28+
readonly value: number;
29+
}[];
30+
31+
const interval = $derived(page.url.searchParams.get('interval') ?? '7d');
32+
33+
function options(
34+
data: resourceUsage,
35+
request: number,
36+
limit?: number,
37+
color: string = '#000000',
38+
valueFormatter: (value: number) => string = (value: number) =>
39+
value == null ? '-' : value.toLocaleString('en-GB', { maximumFractionDigits: 4 })
40+
): EChartsOption {
41+
const safeData = data ?? [];
42+
43+
return {
44+
animation: false,
45+
tooltip: {
46+
trigger: 'axis',
47+
formatter: (value: CallbackDataParams[]) => {
48+
const dot = (color: string) =>
49+
`<div style="height: 8px; width: 8px; border-radius: 50%; background-color: ${color};"></div>`;
50+
const div = document.createElement('div');
51+
52+
const [usage, request, limit] = value;
53+
if (Array.isArray(usage.value) && Array.isArray(request.value)) {
54+
div.innerHTML = `
55+
<div>${format(usage.value.at(0) as number, 'dd/MM/yyyy HH:mm')}</div>
56+
<hr style="border: none; height: 1px; background-color: var(--a-border-subtle);" />
57+
<div style="display: grid; grid-template-columns: auto auto; column-gap: 0.5rem;">
58+
<div style="display: flex; align-items: center; gap: 0.25rem;">${dot(color)}${usage.seriesName}:</div><div style="text-align: right;">${valueFormatter(usage.value.at(1) as number)}</div>
59+
<div style="display: flex; align-items: center; gap: 0.25rem;">${dot(requestColor)}${request.seriesName}:</div><div style="text-align: right;">${valueFormatter(request.value.at(1) as number)}</div>
60+
${Array.isArray(limit?.value) ? `<div style="display: flex; align-items: center; gap: 0.25rem;">${dot(limitColor)}${limit.seriesName}:</div><div style="text-align: right;">${valueFormatter(limit.value.at(1) as number)}</div>` : ''}
61+
</div>
62+
`;
63+
}
64+
return div;
65+
},
66+
axisPointer: {
67+
animation: false
68+
}
69+
},
70+
xAxis: {
71+
type: 'time',
72+
boundaryGap: false,
73+
axisLabel: { formatter: { month: '{MMM} {d}', day: '{dd}.{MM}' } }
74+
},
75+
yAxis: {
76+
type: 'value',
77+
// name: 'Usage of requested resources',
78+
axisLabel: {
79+
formatter: (value: number) => value.toLocaleString('en-GB', { maximumFractionDigits: 4 })
80+
}
81+
},
82+
series: [
83+
{
84+
name: 'Usage',
85+
type: 'line',
86+
data: safeData.map((d) => [d.timestamp.getTime(), d.value]),
87+
showSymbol: false,
88+
color,
89+
areaStyle: {
90+
opacity: 0.2
91+
}
92+
},
93+
{
94+
data: safeData.map((d) => [d.timestamp.getTime(), request]),
95+
type: 'line',
96+
name: 'Requested',
97+
showSymbol: false,
98+
color: requestColor,
99+
lineStyle: { color: requestColor },
100+
markLine: {
101+
symbol: 'none',
102+
data: [
103+
{
104+
yAxis: request,
105+
label: { formatter: 'Requested', position: 'end', color: requestColor },
106+
lineStyle: { type: 'solid', color: 'transparent' }
107+
}
108+
]
109+
}
110+
},
111+
...(limit
112+
? [
113+
{
114+
data: safeData.map((d) => [d.timestamp.getTime(), limit]),
115+
type: 'line',
116+
name: 'Limit',
117+
showSymbol: false,
118+
color: limitColor,
119+
lineStyle: { color: limitColor },
120+
markLine: {
121+
symbol: 'none',
122+
data: [
123+
{
124+
yAxis: limit,
125+
label: { formatter: 'Limit', position: 'end', color: limitColor },
126+
lineStyle: { type: 'solid', color: 'transparent' }
127+
}
128+
]
129+
}
130+
}
131+
]
132+
: [])
133+
]
134+
} as EChartsOption;
135+
}
136+
137+
const limitColor = '#DE2E2E';
138+
const usageMemColor = '#8269A2';
139+
const usageCPUColor = '#FF9100';
140+
const requestColor = '#3386E0';
141+
</script>
142+
143+
<GraphErrors errors={$ResourceUtilizationForApp.errors} />
144+
145+
<div class="wrapper">
146+
<BodyLong>
147+
These graphs help you analyze your app's CPU and memory usage over time.
148+
<ul>
149+
<li>Blue Line (Requests): The guaranteed CPU or memory allocation for your app.</li>
150+
<li>Red Line (Limits, if present): The maximum allowed usage before restrictions apply.</li>
151+
<li>Shaded Area: The actual resource consumption over time.</li>
152+
</ul>
153+
Your app can exceed the request line, which is expected if additional resources are available:
154+
<ul>
155+
<li>
156+
For CPU: Exceeding requests may cause throttling, leading to reduced performance but no
157+
crashes.
158+
</li>
159+
<li>
160+
For Memory: Exceeding the limit causes termination (OOMKilled) because memory cannot be
161+
throttled.
162+
</li>
163+
</ul>
164+
<div>To optimize costs while maintaining performance:</div>
165+
<div>✅ If usage is consistently below requests, consider lowering requests to save money.</div>
166+
<div>✅ If CPU usage is frequently throttled, increasing requests may improve performance.</div>
167+
<div>
168+
✅ If memory usage hits the limit, increasing requests or optimizing memory use may prevent
169+
crashes.
170+
</div>
171+
</BodyLong>
172+
<div class="section">
173+
<Heading level="2" size="medium" spacing>Memory usage</Heading>
174+
{#if $ResourceUtilizationForApp.data}
175+
{@const utilization =
176+
$ResourceUtilizationForApp.data.team.environment.application.utilization}
177+
<BodyLong spacing>
178+
At the latest data point, usage is {(
179+
memoryUtilization(utilization.requested_memory, utilization.current_memory) * 100
180+
).toFixed(0)}% of {prettyBytes(utilization.requested_memory, {
181+
locale: 'en',
182+
minimumFractionDigits: 2,
183+
maximumFractionDigits: 2
184+
})} requested memory. Based on this data point, the estimated annual cost of unused memory of
185+
{euroValueFormatter(
186+
yearlyOverageCost(
187+
UtilizationResourceType.MEMORY,
188+
utilization.requested_memory,
189+
utilization.current_memory
190+
)
191+
)}.
192+
</BodyLong>
193+
<div style="justify-self: end;">
194+
<ToggleGroup
195+
value={interval}
196+
onchange={(interval) => changeParams({ interval }, { noScroll: true })}
197+
>
198+
{#each ['1h', '6h', '1d', '7d', '30d'] as interval (interval)}
199+
<ToggleGroupItem value={interval}>{interval}</ToggleGroupItem>
200+
{/each}
201+
</ToggleGroup>
202+
</div>
203+
<EChart
204+
options={options(
205+
utilization.memory_series.map((d) => {
206+
return { timestamp: d.timestamp, value: d.value / 1024 / 1024 / 1024 };
207+
}),
208+
utilization.requested_memory / 1024 / 1024 / 1024,
209+
utilization.limit_memory ? utilization.limit_memory / 1024 / 1024 / 1024 : undefined,
210+
usageMemColor,
211+
(value) =>
212+
value == null
213+
? '-'
214+
: prettyBytes(value * 1024 ** 3, {
215+
locale: 'en',
216+
minimumFractionDigits: 2,
217+
maximumFractionDigits: 2
218+
})
219+
)}
220+
style="height: 400px"
221+
/>
222+
{:else}
223+
<div style="height: 504px; display: flex; justify-content: center; align-items: center;">
224+
<Loader size="3xlarge" />
225+
</div>
226+
{/if}
227+
</div>
228+
<div class="section">
229+
<Heading level="2" size="medium" spacing>CPU usage</Heading>
230+
{#if $ResourceUtilizationForApp.data}
231+
{@const utilization =
232+
$ResourceUtilizationForApp.data.team.environment.application.utilization}
233+
<BodyLong spacing>
234+
At the latest data point, usage is {cpuUtilization(
235+
utilization.requested_cpu,
236+
utilization.current_cpu
237+
)}% of {utilization.requested_cpu.toLocaleString('en-GB', {
238+
minimumFractionDigits: 2,
239+
maximumFractionDigits: 2
240+
})} requested CPUs. Based on this data point, the estimated annual cost of unused CPU of {euroValueFormatter(
241+
yearlyOverageCost(
242+
UtilizationResourceType.CPU,
243+
utilization.requested_cpu,
244+
utilization.current_cpu
245+
)
246+
)}.
247+
</BodyLong>
248+
<div style="justify-self: end;">
249+
<ToggleGroup
250+
value={interval}
251+
onchange={(interval) => changeParams({ interval }, { noScroll: true })}
252+
>
253+
{#each ['1h', '6h', '1d', '7d', '30d'] as interval (interval)}
254+
<ToggleGroupItem value={interval}>{interval}</ToggleGroupItem>
255+
{/each}
256+
</ToggleGroup>
257+
</div>
258+
<EChart
259+
options={options(
260+
utilization.cpu_series,
261+
utilization.requested_cpu,
262+
utilization.limit_cpu ? utilization.limit_cpu : undefined,
263+
usageCPUColor,
264+
(value: number) =>
265+
value == null
266+
? '-'
267+
: `${value.toLocaleString('en-GB', { maximumFractionDigits: 4 })} CPUs`
268+
)}
269+
style="height: 400px"
270+
/>
271+
{:else}
272+
<div style="height: 504px; display: flex; justify-content: center; align-items: center;">
273+
<Loader size="3xlarge" />
274+
</div>
275+
{/if}
276+
</div>
277+
</div>
278+
279+
<style>
280+
.wrapper {
281+
display: grid;
282+
gap: var(--a-spacing-6);
283+
}
284+
285+
.section {
286+
display: grid;
287+
}
288+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { load_ResourceUtilizationForApp } from '$houdini';
2+
import type { PageLoad } from './$houdini';
3+
4+
export const ssr = false;
5+
export const load: PageLoad = async (event) => {
6+
const interval = event.url.searchParams.get('interval');
7+
const end = new Date(Date.now());
8+
let start = new Date(Date.now() - 7 * 24 * 1000 * 60 * 60);
9+
switch (interval) {
10+
case '1h':
11+
start = new Date(Date.now() - 1000 * 60 * 60);
12+
break;
13+
case '6h':
14+
start = new Date(Date.now() - 6 * 1000 * 60 * 60);
15+
break;
16+
case '1d':
17+
start = new Date(Date.now() - 24 * 1000 * 60 * 60);
18+
break;
19+
case '30d':
20+
start = new Date(Date.now() - 30 * 24 * 1000 * 60 * 60);
21+
break;
22+
}
23+
24+
return {
25+
interval,
26+
...(await load_ResourceUtilizationForApp({
27+
event,
28+
variables: {
29+
app: event.params.app,
30+
env: event.params.env,
31+
team: event.params.team,
32+
start,
33+
end
34+
}
35+
}))
36+
};
37+
};

0 commit comments

Comments
 (0)
Please sign in to comment.