Skip to content

Commit eb1c813

Browse files
authored
feat(cache-chart): Show hits/misses per poll interval (#4146)
1 parent b9f25b0 commit eb1c813

4 files changed

Lines changed: 216 additions & 12 deletions

File tree

spring-boot-admin-server-ui/src/main/frontend/views/instances/details/cache-chart.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export default {
8585
const y = d3
8686
.scaleLinear()
8787
.range([this.height, 0])
88-
.domain([0, d3.max(data, (d) => d.total) * 1.05]);
88+
.domain([0, d3.max(data, (d) => d.totalPerInterval) * 1.05]);
8989
9090
//draw areas
9191
const miss = this.areas
@@ -101,8 +101,8 @@ export default {
101101
d3
102102
.area()
103103
.x((d) => x(d.timestamp))
104-
.y0((d) => y(d.hit))
105-
.y1((d) => y(d.total)),
104+
.y0((d) => y(d.hitsPerInterval))
105+
.y1((d) => y(d.totalPerInterval)),
106106
);
107107
miss.exit().remove();
108108
@@ -118,7 +118,7 @@ export default {
118118
.area()
119119
.x((d) => x(d.timestamp))
120120
.y0(y(0))
121-
.y1((d) => y(d.hit)),
121+
.y1((d) => y(d.hitsPerInterval)),
122122
);
123123
hit.exit().remove();
124124
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { screen, waitFor } from '@testing-library/vue';
2+
import { enableAutoUnmount, shallowMount } from '@vue/test-utils';
3+
import { HttpResponse, http } from 'msw';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5+
6+
import { applications } from '@/mocks/applications/data';
7+
import { server } from '@/mocks/server';
8+
import Application from '@/services/application';
9+
import { render } from '@/test-utils';
10+
import DetailsCache from '@/views/instances/details/details-cache.vue';
11+
12+
vi.mock('@/sba-config', async () => {
13+
const sbaConfig: any = await vi.importActual('@/sba-config');
14+
return {
15+
default: {
16+
...sbaConfig.default,
17+
uiSettings: {
18+
pollTimer: {
19+
cache: 100,
20+
},
21+
},
22+
},
23+
};
24+
});
25+
26+
describe('DetailsCache', () => {
27+
enableAutoUnmount(afterEach);
28+
29+
const CACHE_NAME = 'foo-cache';
30+
const HITS = [24.0, 32.0, 48.0];
31+
const MISSES = [8.0, 8.0, 16.0];
32+
const TOTAL = [32, 40, 64];
33+
const HITS_PER_INTERVAL = [0, 8, 16];
34+
const MISSES_PER_INTERVAL = [0, 0, 8];
35+
const TOTAL_PER_INTERVAL = [0, 8, 24];
36+
37+
beforeEach(() => {
38+
const hitsGenerator = (function* () {
39+
yield* HITS;
40+
})();
41+
const missesGenerator = (function* () {
42+
yield* MISSES;
43+
})();
44+
45+
server.use(
46+
http.get(
47+
'/instances/:instanceId/actuator/metrics/cache.gets',
48+
({ request }) => {
49+
const url = new URL(request.url);
50+
const tags = url.searchParams.getAll('tag');
51+
52+
if (tags.includes('result:hit')) {
53+
return HttpResponse.json({
54+
measurements: [{ value: hitsGenerator.next()?.value }],
55+
});
56+
} else if (tags.includes('result:miss')) {
57+
return HttpResponse.json({
58+
measurements: [{ value: missesGenerator.next()?.value }],
59+
});
60+
}
61+
},
62+
),
63+
http.get('/instances/:instanceId/actuator/metrics/cache.size', () => {
64+
return HttpResponse.json({
65+
measurements: [{ value: '1337' }],
66+
});
67+
}),
68+
);
69+
});
70+
71+
async function renderComponent(stubs = {}) {
72+
const application = new Application(applications[0]);
73+
const instance = application.instances[0];
74+
return render(DetailsCache, {
75+
global: {
76+
stubs,
77+
},
78+
props: {
79+
instance,
80+
cacheName: CACHE_NAME,
81+
index: 0,
82+
},
83+
});
84+
}
85+
86+
it('should render cache name', async () => {
87+
await renderComponent();
88+
89+
expect(
90+
await screen.findByRole('heading', { name: `Cache: ${CACHE_NAME}` }),
91+
).toBeVisible();
92+
});
93+
94+
it('should render hits and misses', async () => {
95+
await renderComponent();
96+
97+
expect(
98+
await screen.findByLabelText('instances.details.cache.hits'),
99+
).toHaveTextContent(`${HITS[0]}`);
100+
expect(
101+
await screen.findByLabelText('instances.details.cache.misses'),
102+
).toHaveTextContent(`${MISSES[0]}`);
103+
});
104+
105+
it('should calculate and render hit/miss ratio', async () => {
106+
await renderComponent();
107+
108+
expect(
109+
await screen.findByLabelText('instances.details.cache.hit_ratio'),
110+
).toHaveTextContent('75.00%'); // 24 hits, 8 misses
111+
});
112+
113+
it('should calculate total', async () => {
114+
const application = new Application(applications[0]);
115+
const instance = application.instances[0];
116+
117+
const vueWrapper = shallowMount(DetailsCache, {
118+
props: {
119+
instance,
120+
cacheName: CACHE_NAME,
121+
index: 0,
122+
},
123+
});
124+
125+
await waitFor(() => {
126+
expect(vueWrapper.vm.chartData).toHaveLength(3);
127+
});
128+
129+
for (let index = 0; index < vueWrapper.vm.chartData.length; index++) {
130+
expect(vueWrapper.vm.chartData[index].total).toEqual(TOTAL[index]);
131+
}
132+
});
133+
134+
it('should calculate hits, misses and total per interval', async () => {
135+
const application = new Application(applications[0]);
136+
const instance = application.instances[0];
137+
138+
const vueWrapper = shallowMount(DetailsCache, {
139+
props: {
140+
instance,
141+
cacheName: CACHE_NAME,
142+
index: 0,
143+
},
144+
});
145+
146+
await waitFor(() => {
147+
expect(vueWrapper.vm.chartData).toHaveLength(3);
148+
});
149+
150+
for (let index = 0; index < vueWrapper.vm.chartData.length; index++) {
151+
expect(vueWrapper.vm.chartData[index].hitsPerInterval).toEqual(
152+
HITS_PER_INTERVAL[index],
153+
);
154+
expect(vueWrapper.vm.chartData[index].missesPerInterval).toEqual(
155+
MISSES_PER_INTERVAL[index],
156+
);
157+
expect(vueWrapper.vm.chartData[index].totalPerInterval).toEqual(
158+
TOTAL_PER_INTERVAL[index],
159+
);
160+
}
161+
});
162+
});

spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-cache.vue

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,50 @@
2323
>
2424
<template v-if="current.hit !== undefined">
2525
<dt
26+
:id="`metrics.cache.${index}.hits`"
2627
class="text-sm font-medium text-gray-500 sm:col-span-4"
2728
v-text="$t('instances.details.cache.hits')"
2829
/>
2930
<dd
31+
:aria-labelledby="`metrics.cache.${index}.hits`"
3032
class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"
3133
v-text="current.hit"
3234
/>
3335
</template>
3436
<template v-if="current.miss !== undefined">
3537
<dt
38+
:id="`metrics.cache.${index}.misses`"
3639
class="text-sm font-medium text-gray-500 sm:col-span-4"
3740
v-text="$t('instances.details.cache.misses')"
3841
/>
3942
<dd
43+
:aria-labelledby="`metrics.cache.${index}.misses`"
4044
class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"
4145
v-text="current.miss"
4246
/>
4347
</template>
4448
<template v-if="ratio !== undefined">
4549
<dt
46-
class="sm:col-span-4"
50+
:id="`metrics.cache.${index}.ratio`"
51+
class="text-sm font-medium text-gray-500 sm:col-span-4"
4752
v-text="$t('instances.details.cache.hit_ratio')"
4853
/>
49-
<dd v-text="ratio" />
54+
<dd
55+
:aria-labelledby="`metrics.cache.${index}.ratio`"
56+
class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2"
57+
v-text="ratio"
58+
/>
5059
</template>
5160
<template v-if="current.size !== undefined">
52-
<dt class="sm:col-span-4" v-text="$t('instances.details.cache.size')" />
53-
<dd v-text="current.size" />
61+
<dt
62+
:id="`metrics.cache.${index}.size`"
63+
class="sm:col-span-4"
64+
v-text="$t('instances.details.cache.size')"
65+
/>
66+
<dd
67+
:aria-labelledby="`metrics.cache.${index}.size`"
68+
v-text="current.size"
69+
/>
5470
</template>
5571
</dl>
5672
<cache-chart v-if="chartData.length > 0" :data="chartData" />
@@ -61,14 +77,17 @@
6177
import moment from 'moment';
6278
import { take } from 'rxjs/operators';
6379
80+
import sbaAlert from '@/components/sba-alert.vue';
81+
import sbaPanel from '@/components/sba-panel.vue';
82+
6483
import subscribing from '@/mixins/subscribing';
6584
import sbaConfig from '@/sba-config';
6685
import Instance from '@/services/instance';
67-
import { concatMap, delay, retryWhen, timer } from '@/utils/rxjs';
86+
import { concatMap, delay, map, retryWhen, timer } from '@/utils/rxjs';
6887
import cacheChart from '@/views/instances/details/cache-chart';
6988
7089
export default {
71-
components: { cacheChart },
90+
components: { sbaAlert, sbaPanel, cacheChart },
7291
mixins: [subscribing],
7392
props: {
7493
instance: {
@@ -79,6 +98,10 @@ export default {
7998
type: String,
8099
required: true,
81100
},
101+
index: {
102+
type: Number,
103+
required: true,
104+
},
82105
},
83106
data: () => ({
84107
hasLoaded: false,
@@ -91,7 +114,10 @@ export default {
91114
}),
92115
computed: {
93116
ratio() {
94-
if (Number.isFinite(this.current.hit) && Number.isFinite(this.current)) {
117+
if (
118+
Number.isFinite(this.current.hit) &&
119+
Number.isFinite(this.current.miss)
120+
) {
95121
const total = this.current.hit + this.current.miss;
96122
return total > 0
97123
? ((this.current.hit / total) * 100).toFixed(2) + '%'
@@ -167,10 +193,25 @@ export default {
167193
}
168194
}
169195
},
196+
calculateMetricsPerInterval(data) {
197+
let hitsPerInterval = 0;
198+
let missesPerInterval = 0;
199+
let totalPerInterval = 0;
200+
201+
if (this.chartData.length > 0) {
202+
const previousChartData = this.chartData[this.chartData.length - 1];
203+
hitsPerInterval = data.hit - previousChartData.hit;
204+
missesPerInterval = data.miss - previousChartData.miss;
205+
totalPerInterval = data.total - previousChartData.total;
206+
}
207+
208+
return { ...data, hitsPerInterval, missesPerInterval, totalPerInterval };
209+
},
170210
createSubscription() {
171211
return timer(0, sbaConfig.uiSettings.pollTimer.cache)
172212
.pipe(
173213
concatMap(this.fetchMetrics),
214+
map(this.calculateMetricsPerInterval),
174215
retryWhen((err) => {
175216
return err.pipe(delay(1000), take(5));
176217
}),

spring-boot-admin-server-ui/src/main/frontend/views/instances/details/details-caches.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
<template>
1818
<div>
1919
<details-cache
20-
v-for="cache in caches"
20+
v-for="(cache, index) in caches"
2121
:key="cache"
22+
:index="index"
2223
:cache-name="cache"
2324
:instance="instance"
2425
/>

0 commit comments

Comments
 (0)