From 1651d17823da16ab355b2824397686619450b405 Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Tue, 6 Jan 2026 10:55:55 +1100 Subject: [PATCH 01/27] Added load tests and configured existing ones --- .../uid2-operator/k6-identity-map.js | 8 +- .../k6-token-generate-identitymaplarge.js | 445 ++++++++++++++++++ .../uid2-operator/k6-token-generate.js | 2 +- .../start-generate-identitymaplarge.sh | 9 + 4 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 performance-testing/uid2-operator/k6-token-generate-identitymaplarge.js create mode 100644 performance-testing/uid2-operator/start-generate-identitymaplarge.sh diff --git a/performance-testing/uid2-operator/k6-identity-map.js b/performance-testing/uid2-operator/k6-identity-map.js index 3b03d2f..de404e2 100755 --- a/performance-testing/uid2-operator/k6-identity-map.js +++ b/performance-testing/uid2-operator/k6-identity-map.js @@ -8,10 +8,10 @@ const baseUrl = __ENV.OPERATOR_URL; const clientSecret = __ENV.CLIENT_SECRET; const clientKey = __ENV.CLIENT_KEY; const identityMapVUs = 300; -const identityMapLargeBatchVUs = 10; +const identityMapLargeBatchVUs = 30; const generateVUs = vus; -const testDuration = '5m' +const testDuration = '15m' export const options = { insecureSkipTLSVerify: true, @@ -47,9 +47,9 @@ export const options = { executor: 'constant-vus', exec: 'identityMapLargeBatch', vus: identityMapLargeBatchVUs, - duration: '300s', + duration: testDuration, gracefulStop: '0s', - startTime: '40s', + startTime: '30s', }, }, // So we get count in the summary, to demonstrate different metrics are different diff --git a/performance-testing/uid2-operator/k6-token-generate-identitymaplarge.js b/performance-testing/uid2-operator/k6-token-generate-identitymaplarge.js new file mode 100644 index 0000000..42bed97 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-identitymaplarge.js @@ -0,0 +1,445 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateVUs = 300; +const refreshVUs = vus; +const identityMapVUs = 30; +const keySharingVUs = vus; +const testDuration = '15m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-vus', + exec: 'tokenGenerate', + stages: [ + { duration: '30s', target: generateVUs} + ], + gracefulRampDown: '0s', + }, + // tokenRefreshWarmup: { + // executor: 'ramping-vus', + // exec: 'tokenRefresh', + // stages: [ + // { duration: '30s', target: refreshVUs} + // ], + // gracefulRampDown: '0s', + // }, + identityMapWarmup: { + executor: 'ramping-vus', + exec: 'identityMap', + stages: [ + { duration: '30s', target: generateVUs} + ], + gracefulRampDown: '0s', + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: '30s', target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-vus', + exec: 'tokenGenerate', + vus: generateVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + }, + // tokenRefresh: { + // executor: 'constant-vus', + // exec: 'tokenRefresh', + // vus: refreshVUs, + // duration: testDuration, + // gracefulStop: '0s', + // startTime: '30s', + // }, + // identityMap: { + // executor: 'constant-vus', + // exec: 'identityMapLargeBatch', + // vus: identityMapVUs, + // duration: testDuration, + // gracefulStop: '0s', + // startTime: '30s', + // }, + /* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },*/ + // identityMapLargeBatchSequential: { + // executor: 'constant-vus', + // exec: 'identityMapLargeBatch', + // vus: 1, + // duration: '300s', + // gracefulStop: '0s', + // startTime: '970s', + // }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: identityMapVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + }, + // identityBuckets: { + // executor: 'constant-vus', + // exec: 'identityBuckets', + // vus: 2, + // duration: '300s', + // gracefulStop: '0s', + // startTime: '1590s', + // }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let request = await createReq( {'optout_check': 1, 'email': 'test5000@example.com'}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(2);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + "email": [] + }; + + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let requestData = { 'optout_check': 1, 'email': 'test500@example.com' }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/k6-token-generate.js b/performance-testing/uid2-operator/k6-token-generate.js index 6f10b36..588d81a 100755 --- a/performance-testing/uid2-operator/k6-token-generate.js +++ b/performance-testing/uid2-operator/k6-token-generate.js @@ -9,7 +9,7 @@ const clientSecret = __ENV.CLIENT_SECRET; const clientKey = __ENV.CLIENT_KEY; const generateVUs = vus; -const testDuration = '5m' +const testDuration = '15m' export const options = { insecureSkipTLSVerify: true, diff --git a/performance-testing/uid2-operator/start-generate-identitymaplarge.sh b/performance-testing/uid2-operator/start-generate-identitymaplarge.sh new file mode 100644 index 0000000..a5f9d8a --- /dev/null +++ b/performance-testing/uid2-operator/start-generate-identitymaplarge.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-identitymaplarge.js $COMMENT \ No newline at end of file From a5234ac4e7c84e6935ff09848874596cc0f186df Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Tue, 13 Jan 2026 15:57:20 +1100 Subject: [PATCH 02/27] Performance scenarios from report --- ...generate-refresh-identitymap-scenario-1.js | 461 ++++++++++++++++++ ...generate-refresh-identitymap-scenario-2.js | 461 ++++++++++++++++++ .../uid2-operator/start-scenario-1.sh | 9 + .../uid2-operator/start-scenario-2.sh | 9 + 4 files changed, 940 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js create mode 100644 performance-testing/uid2-operator/start-scenario-1.sh create mode 100644 performance-testing/uid2-operator/start-scenario-2.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js new file mode 100644 index 0000000..58b82b2 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -0,0 +1,461 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 500000; +const refreshRPS = 25000; +const identityMapRPS = 1500; +const testDuration = '15m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 500, + maxVUs: 1000, + stages: [ + { duration: '30s', target: generateRPS} + ], + gracefulRampDown: '0s', + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: '30s', target: refreshRPS} + ], + gracefulRampDown: '0s', + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + stages: [ + { duration: '30s', target: identityMapRPS} + ], + gracefulRampDown: '0s', + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: '30s', target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 500, + maxVUs: 1000, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let request = await createReq( {'optout_check': 1, 'email': 'test5000@example.com'}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(100);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + "email": [] + }; + + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let requestData = { 'optout_check': 1, 'email': 'test500@example.com' }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js new file mode 100644 index 0000000..90863c2 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js @@ -0,0 +1,461 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; +const identityMapRPS = 1500; +const testDuration = '15m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: '30s', target: generateRPS} + ], + gracefulRampDown: '0s', + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: '30s', target: refreshRPS} + ], + gracefulRampDown: '0s', + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 500, + maxVUs: 1000, + stages: [ + { duration: '30s', target: identityMapRPS} + ], + gracefulRampDown: '0s', + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: '30s', target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 500, + maxVUs: 1000, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let request = await createReq( {'optout_check': 1, 'email': 'test5000@example.com'}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + "email": [] + }; + + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let requestData = { 'optout_check': 1, 'email': 'test500@example.com' }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-1.sh b/performance-testing/uid2-operator/start-scenario-1.sh new file mode 100644 index 0000000..5b1c559 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-1.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-1.js $COMMENT \ No newline at end of file diff --git a/performance-testing/uid2-operator/start-scenario-2.sh b/performance-testing/uid2-operator/start-scenario-2.sh new file mode 100644 index 0000000..64a3542 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-2.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-2.js $COMMENT \ No newline at end of file From 7dfb590ed8b79815f874b6be756927e50472eeb1 Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Tue, 13 Jan 2026 16:18:07 +1100 Subject: [PATCH 03/27] Removed graceful rampdown due to errors --- .../k6-token-generate-refresh-identitymap-scenario-1.js | 3 --- .../k6-token-generate-refresh-identitymap-scenario-2.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js index 58b82b2..0047897 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -26,7 +26,6 @@ export const options = { stages: [ { duration: '30s', target: generateRPS} ], - gracefulRampDown: '0s', }, tokenRefreshWarmup: { executor: 'ramping-arrival-rate', @@ -37,7 +36,6 @@ export const options = { stages: [ { duration: '30s', target: refreshRPS} ], - gracefulRampDown: '0s', }, identityMapWarmup: { executor: 'ramping-arrival-rate', @@ -48,7 +46,6 @@ export const options = { stages: [ { duration: '30s', target: identityMapRPS} ], - gracefulRampDown: '0s', },/* keySharingWarmup: { executor: 'ramping-vus', diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js index 90863c2..56e66a7 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js @@ -26,7 +26,6 @@ export const options = { stages: [ { duration: '30s', target: generateRPS} ], - gracefulRampDown: '0s', }, tokenRefreshWarmup: { executor: 'ramping-arrival-rate', @@ -37,7 +36,6 @@ export const options = { stages: [ { duration: '30s', target: refreshRPS} ], - gracefulRampDown: '0s', }, identityMapWarmup: { executor: 'ramping-arrival-rate', @@ -48,7 +46,6 @@ export const options = { stages: [ { duration: '30s', target: identityMapRPS} ], - gracefulRampDown: '0s', },/* keySharingWarmup: { executor: 'ramping-vus', From 00a23bb4f8d46e163ab87f1b9e76d04bb3c80e6f Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Wed, 14 Jan 2026 10:04:44 +1100 Subject: [PATCH 04/27] Tuned VUs, RPS and duration --- ...generate-refresh-identitymap-scenario-1.js | 28 ++++++++++--------- ...generate-refresh-identitymap-scenario-2.js | 8 +++--- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js index 0047897..8612ac6 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -7,10 +7,12 @@ const baseUrl = __ENV.OPERATOR_URL; const clientSecret = __ENV.CLIENT_SECRET; const clientKey = __ENV.CLIENT_KEY; -const generateRPS = 500000; +const generateRPS = 450000; const refreshRPS = 25000; const identityMapRPS = 1500; -const testDuration = '15m' + +const warmUpTime = '10m' +const testDuration = '30m' export const options = { insecureSkipTLSVerify: true, @@ -21,10 +23,10 @@ export const options = { executor: 'ramping-arrival-rate', exec: 'tokenGenerate', timeUnit: '1s', - preAllocatedVUs: 500, - maxVUs: 1000, + preAllocatedVUs: 1500, + maxVUs: 2000, stages: [ - { duration: '30s', target: generateRPS} + { duration: warmUpTime, target: generateRPS} ], }, tokenRefreshWarmup: { @@ -34,7 +36,7 @@ export const options = { preAllocatedVUs: 200, maxVUs: 400, stages: [ - { duration: '30s', target: refreshRPS} + { duration: warmUpTime, target: refreshRPS} ], }, identityMapWarmup: { @@ -44,14 +46,14 @@ export const options = { preAllocatedVUs: 75, maxVUs: 150, stages: [ - { duration: '30s', target: identityMapRPS} + { duration: warmUpTime, target: identityMapRPS} ], },/* keySharingWarmup: { executor: 'ramping-vus', exec: 'keySharing', stages: [ - { duration: '30s', target: keySharingVUs} + { duration: warmUpTime, target: keySharingVUs} ], gracefulRampDown: '0s', },*/ @@ -61,11 +63,11 @@ export const options = { exec: 'tokenGenerate', rate: generateRPS, timeUnit: '1s', - preAllocatedVUs: 500, - maxVUs: 1000, + preAllocatedVUs: 1500, + maxVUs: 2000, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, }, tokenRefresh: { executor: 'constant-arrival-rate', @@ -76,7 +78,7 @@ export const options = { maxVUs: 400, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, }, identityMap: { executor: 'constant-arrival-rate', @@ -87,7 +89,7 @@ export const options = { maxVUs: 150, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, },/* keySharing:{ executor: 'constant-vus', diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js index 56e66a7..deba87c 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js @@ -41,8 +41,8 @@ export const options = { executor: 'ramping-arrival-rate', exec: 'identityMap', timeUnit: '1s', - preAllocatedVUs: 500, - maxVUs: 1000, + preAllocatedVUs: 1000, + maxVUs: 1500, stages: [ { duration: '30s', target: identityMapRPS} ], @@ -83,8 +83,8 @@ export const options = { exec: 'identityMap', rate: identityMapRPS, timeUnit: '1s', - preAllocatedVUs: 500, - maxVUs: 1000, + preAllocatedVUs: 1000, + maxVUs: 1500, duration: testDuration, gracefulStop: '0s', startTime: '30s', From 23f0410ad14b6f9e41c982ad6d64baeb575274f3 Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Wed, 14 Jan 2026 11:34:50 +1100 Subject: [PATCH 05/27] Added random suffixes to emails and reduced test time --- ...generate-refresh-identitymap-scenario-1.js | 12 +++++--- ...generate-refresh-identitymap-scenario-2.js | 29 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js index 8612ac6..cfada92 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -12,7 +12,8 @@ const refreshRPS = 25000; const identityMapRPS = 1500; const warmUpTime = '10m' -const testDuration = '30m' +const testDuration = '20m' + export const options = { insecureSkipTLSVerify: true, @@ -154,7 +155,8 @@ export async function setup() { }; async function generateRefreshRequest() { - let request = await createReq( {'optout_check': 1, 'email': 'test5000@example.com'}); + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); var requestData = { endpoint: '/v2/token/generate', requestBody: request, @@ -269,8 +271,9 @@ function generateIdentityMapRequest(emailCount) { "email": [] }; + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); for (var i = 0; i < emailCount; ++i) { - data.email.push(`test${i}@example.com`); + data.email.push(`test${randomSuffix}${i}@example.com`); } return data; @@ -436,7 +439,8 @@ async function generateRequestWithTime(obj) { async function generateTokenGenerateRequestWithTime() { - let requestData = { 'optout_check': 1, 'email': 'test500@example.com' }; + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; return await generateRequestWithTime(requestData); } diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js index deba87c..8ff7b08 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2.js @@ -10,7 +10,9 @@ const clientKey = __ENV.CLIENT_KEY; const generateRPS = 25000; const refreshRPS = 25000; const identityMapRPS = 1500; -const testDuration = '15m' + +const warmUpTime = '10m' +const testDuration = '20m' export const options = { insecureSkipTLSVerify: true, @@ -24,7 +26,7 @@ export const options = { preAllocatedVUs: 200, maxVUs: 400, stages: [ - { duration: '30s', target: generateRPS} + { duration: warmUpTime, target: generateRPS} ], }, tokenRefreshWarmup: { @@ -34,7 +36,7 @@ export const options = { preAllocatedVUs: 200, maxVUs: 400, stages: [ - { duration: '30s', target: refreshRPS} + { duration: warmUpTime, target: refreshRPS} ], }, identityMapWarmup: { @@ -44,14 +46,14 @@ export const options = { preAllocatedVUs: 1000, maxVUs: 1500, stages: [ - { duration: '30s', target: identityMapRPS} + { duration: warmUpTime, target: identityMapRPS} ], },/* keySharingWarmup: { executor: 'ramping-vus', exec: 'keySharing', stages: [ - { duration: '30s', target: keySharingVUs} + { duration: warmUpTime, target: keySharingVUs} ], gracefulRampDown: '0s', },*/ @@ -65,7 +67,7 @@ export const options = { maxVUs: 400, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, }, tokenRefresh: { executor: 'constant-arrival-rate', @@ -76,7 +78,7 @@ export const options = { maxVUs: 400, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, }, identityMap: { executor: 'constant-arrival-rate', @@ -87,7 +89,7 @@ export const options = { maxVUs: 1500, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, },/* keySharing:{ executor: 'constant-vus', @@ -95,7 +97,7 @@ export const options = { vus: keySharingVUs, duration: testDuration, gracefulStop: '0s', - startTime: '30s', + startTime: warmUpTime, },*/ /*identityMapLargeBatchSequential: { executor: 'constant-vus', @@ -152,7 +154,8 @@ export async function setup() { }; async function generateRefreshRequest() { - let request = await createReq( {'optout_check': 1, 'email': 'test5000@example.com'}); + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); var requestData = { endpoint: '/v2/token/generate', requestBody: request, @@ -267,8 +270,9 @@ function generateIdentityMapRequest(emailCount) { "email": [] }; + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); for (var i = 0; i < emailCount; ++i) { - data.email.push(`test${i}@example.com`); + data.email.push(`test${randomSuffix}${i}@example.com`); } return data; @@ -434,7 +438,8 @@ async function generateRequestWithTime(obj) { async function generateTokenGenerateRequestWithTime() { - let requestData = { 'optout_check': 1, 'email': 'test500@example.com' }; + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; return await generateRequestWithTime(requestData); } From ba26cb23902cfa4d0c1ca39adb32ea075f0e229c Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Thu, 15 Jan 2026 14:11:43 +1100 Subject: [PATCH 06/27] Increased token gen vus --- .../k6-token-generate-refresh-identitymap-scenario-1.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js index cfada92..3081735 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -25,7 +25,7 @@ export const options = { exec: 'tokenGenerate', timeUnit: '1s', preAllocatedVUs: 1500, - maxVUs: 2000, + maxVUs: 2500, stages: [ { duration: warmUpTime, target: generateRPS} ], @@ -64,8 +64,8 @@ export const options = { exec: 'tokenGenerate', rate: generateRPS, timeUnit: '1s', - preAllocatedVUs: 1500, - maxVUs: 2000, + preAllocatedVUs: 1800, + maxVUs: 2500, duration: testDuration, gracefulStop: '0s', startTime: warmUpTime, From defd943624a08db9f92dfcfe66ab503e72fb436f Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Wed, 28 Jan 2026 14:58:08 +1100 Subject: [PATCH 07/27] Made identitymap requests a distribution --- .../k6-token-generate-refresh-identitymap-scenario-1.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js index 3081735..a318f8c 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -205,7 +205,11 @@ export function tokenRefresh(data) { export async function identityMap(data) { const endpoint = '/v2/identity/map'; if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { - data.identityMap = await generateIdentityMapRequestWithTime(100);; + var dii = 100; + if (Math.random() < 0.01) { + dii = 5000; + } + data.identityMap = await generateIdentityMapRequestWithTime(dii);; } var requestBody = data.identityMap.requestBody; From cecc77c86c6f668985013c2f40c333ce4c09e60c Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Wed, 28 Jan 2026 15:00:29 +1100 Subject: [PATCH 08/27] Moved distribution to scenario 3 --- ...generate-refresh-identitymap-scenario-1.js | 3 - ...generate-refresh-identitymap-scenario-3.js | 468 ++++++++++++++++++ .../uid2-operator/start-scenario-3.sh | 9 + 3 files changed, 477 insertions(+), 3 deletions(-) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js create mode 100644 performance-testing/uid2-operator/start-scenario-3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js index a318f8c..e2dd03e 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1.js @@ -206,9 +206,6 @@ export async function identityMap(data) { const endpoint = '/v2/identity/map'; if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { var dii = 100; - if (Math.random() < 0.01) { - dii = 5000; - } data.identityMap = await generateIdentityMapRequestWithTime(dii);; } diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js new file mode 100644 index 0000000..a318f8c --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js @@ -0,0 +1,468 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 450000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 1500, + maxVUs: 2500, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 1800, + maxVUs: 2500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + var dii = 100; + if (Math.random() < 0.01) { + dii = 5000; + } + data.identityMap = await generateIdentityMapRequestWithTime(dii);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-3.sh b/performance-testing/uid2-operator/start-scenario-3.sh new file mode 100644 index 0000000..bafbe00 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-3.js $COMMENT \ No newline at end of file From 82511e22aa1a0aca69be3150ad2698b70c9e5756 Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Fri, 30 Jan 2026 10:04:12 +1100 Subject: [PATCH 09/27] Added scenario 4 --- ...generate-refresh-identitymap-scenario-4.js | 463 ++++++++++++++++++ .../uid2-operator/start-scenario-4.sh | 9 + 2 files changed, 472 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js create mode 100644 performance-testing/uid2-operator/start-scenario-4.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js new file mode 100644 index 0000000..5025c1e --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js @@ -0,0 +1,463 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 50000; +const refreshRPS = 50000; +const identityMapRPS = 6000; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 2500, + maxVUs: 5000, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 2500, + maxVUs: 5000, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-4.sh b/performance-testing/uid2-operator/start-scenario-4.sh new file mode 100644 index 0000000..fd43a5a --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-4.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-4.js $COMMENT \ No newline at end of file From ad9fc7dc5847a73570501dc20c23ba259db43fdc Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Mon, 2 Feb 2026 12:05:00 +1100 Subject: [PATCH 10/27] Increased large batch odds in scenario 3 to 2% --- .../k6-token-generate-refresh-identitymap-scenario-3.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js index a318f8c..1613f76 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3.js @@ -10,6 +10,7 @@ const clientKey = __ENV.CLIENT_KEY; const generateRPS = 450000; const refreshRPS = 25000; const identityMapRPS = 1500; +const largeBatchChance = 0.02; const warmUpTime = '10m' const testDuration = '20m' @@ -206,7 +207,7 @@ export async function identityMap(data) { const endpoint = '/v2/identity/map'; if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { var dii = 100; - if (Math.random() < 0.01) { + if (Math.random() < largeBatchChance) { dii = 5000; } data.identityMap = await generateIdentityMapRequestWithTime(dii);; From bcdf77dd21a0380f25e6e347b38176ade676ee95 Mon Sep 17 00:00:00 2001 From: Samin Rahman Date: Fri, 20 Feb 2026 15:19:10 +1100 Subject: [PATCH 11/27] Updated high DII stress tests script --- .../uid2-operator/k6-test-resource.yml | 2 +- ...generate-refresh-identitymap-scenario-4.js | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/performance-testing/uid2-operator/k6-test-resource.yml b/performance-testing/uid2-operator/k6-test-resource.yml index d3b5679..c623bcf 100644 --- a/performance-testing/uid2-operator/k6-test-resource.yml +++ b/performance-testing/uid2-operator/k6-test-resource.yml @@ -3,7 +3,7 @@ kind: TestRun metadata: name: k6-uid2-load-test spec: - parallelism: 10 # original 4 + parallelism: 20 # original 4 arguments: --out experimental-prometheus-rw --tag "testid=replacecomment" script: configMap: diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js index 5025c1e..44fbeb8 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4.js @@ -9,9 +9,9 @@ const clientKey = __ENV.CLIENT_KEY; const generateRPS = 50000; const refreshRPS = 50000; -const identityMapRPS = 6000; +const identityMapRPS = 8000; -const warmUpTime = '10m' +const warmUpTime = '20m' const testDuration = '20m' export const options = { @@ -24,7 +24,7 @@ export const options = { exec: 'tokenGenerate', timeUnit: '1s', preAllocatedVUs: 200, - maxVUs: 400, + maxVUs: 500, stages: [ { duration: warmUpTime, target: generateRPS} ], @@ -34,7 +34,7 @@ export const options = { exec: 'tokenRefresh', timeUnit: '1s', preAllocatedVUs: 200, - maxVUs: 400, + maxVUs: 500, stages: [ { duration: warmUpTime, target: refreshRPS} ], @@ -43,8 +43,8 @@ export const options = { executor: 'ramping-arrival-rate', exec: 'identityMap', timeUnit: '1s', - preAllocatedVUs: 2500, - maxVUs: 5000, + preAllocatedVUs: 3000, + maxVUs: 8000, stages: [ { duration: warmUpTime, target: identityMapRPS} ], @@ -64,7 +64,7 @@ export const options = { rate: generateRPS, timeUnit: '1s', preAllocatedVUs: 200, - maxVUs: 400, + maxVUs: 500, duration: testDuration, gracefulStop: '0s', startTime: warmUpTime, @@ -75,7 +75,7 @@ export const options = { rate: refreshRPS, timeUnit: '1s', preAllocatedVUs: 200, - maxVUs: 400, + maxVUs: 500, duration: testDuration, gracefulStop: '0s', startTime: warmUpTime, @@ -85,8 +85,8 @@ export const options = { exec: 'identityMap', rate: identityMapRPS, timeUnit: '1s', - preAllocatedVUs: 2500, - maxVUs: 5000, + preAllocatedVUs: 3000, + maxVUs: 8000, duration: testDuration, gracefulStop: '0s', startTime: warmUpTime, From f9fa928e1e4827fe7a776facd1acb5902067780b Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 27 Feb 2026 13:21:49 +1100 Subject: [PATCH 12/27] add /v3/identity/map version of scenario-1 --- ...erate-refresh-identitymap-scenario-1-v3.js | 464 ++++++++++++++++++ .../uid2-operator/start-scenario-1-v3.sh | 9 + 2 files changed, 473 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-1-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1-v3.js new file mode 100644 index 0000000..845f4b6 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-1-v3.js @@ -0,0 +1,464 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 450000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 1500, + maxVUs: 2500, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 1800, + maxVUs: 2500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + var dii = 100; + data.identityMap = await generateIdentityMapRequestWithTime(dii);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-1-v3.sh b/performance-testing/uid2-operator/start-scenario-1-v3.sh new file mode 100644 index 0000000..fbf778f --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-1-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-1-v3.js $COMMENT From 487b0e9707aaa9c120861b32acf5020c7c28882e Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 27 Feb 2026 14:42:07 +1100 Subject: [PATCH 13/27] add v3 version of scenario-2 --- ...erate-refresh-identitymap-scenario-2-v3.js | 462 ++++++++++++++++++ .../uid2-operator/start-scenario-2-v3.sh | 9 + 2 files changed, 471 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-2-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2-v3.js new file mode 100644 index 0000000..c527aa3 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-2-v3.js @@ -0,0 +1,462 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-2-v3.sh b/performance-testing/uid2-operator/start-scenario-2-v3.sh new file mode 100644 index 0000000..1cacc38 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-2-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-2-v3.js $COMMENT From 50d4fa76ff3754835b1be6086c50db9a13cbb969 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 27 Feb 2026 22:53:05 +1100 Subject: [PATCH 14/27] add v3 version of scenario-3/4 --- ...erate-refresh-identitymap-scenario-3-v3.js | 468 ++++++++++++++++++ ...erate-refresh-identitymap-scenario-4-v3.js | 462 +++++++++++++++++ .../uid2-operator/start-scenario-3-v3.sh | 9 + .../uid2-operator/start-scenario-4-v3.sh | 9 + 4 files changed, 948 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3-v3.js create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-3-v3.sh create mode 100644 performance-testing/uid2-operator/start-scenario-4-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3-v3.js new file mode 100644 index 0000000..7c6a411 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-3-v3.js @@ -0,0 +1,468 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 450000; +const refreshRPS = 25000; +const identityMapRPS = 1500; +const largeBatchChance = 0.02; + +const warmUpTime = '10m' +const testDuration = '20m' + + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 1500, + maxVUs: 2500, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 1800, + maxVUs: 2500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 75, + maxVUs: 150, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: '30s', + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + var dii = 100; + if (Math.random() < largeBatchChance) { + dii = 5000; + } + data.identityMap = await generateIdentityMapRequestWithTime(dii);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4-v3.js new file mode 100644 index 0000000..3377894 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-4-v3.js @@ -0,0 +1,462 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 50000; +const refreshRPS = 50000; +const identityMapRPS = 8000; + +const warmUpTime = '20m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 500, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 500, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 3000, + maxVUs: 8000, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 3000, + maxVUs: 8000, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-3-v3.sh b/performance-testing/uid2-operator/start-scenario-3-v3.sh new file mode 100644 index 0000000..bb354f0 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-3-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-3-v3.js $COMMENT diff --git a/performance-testing/uid2-operator/start-scenario-4-v3.sh b/performance-testing/uid2-operator/start-scenario-4-v3.sh new file mode 100644 index 0000000..fbb88d8 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-4-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-4-v3.js $COMMENT From eb6f2c9b291c83c3c8b7a8c4ea4df12f7bb4ab43 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 4 Mar 2026 08:32:12 +1100 Subject: [PATCH 15/27] add scenario 5 --- ...erate-refresh-identitymap-scenario-5-v3.js | 462 +++++++++++++++++ ...generate-refresh-identitymap-scenario-5.js | 463 ++++++++++++++++++ .../uid2-operator/start-scenario-5-v3.sh | 9 + .../uid2-operator/start-scenario-5.sh | 9 + 4 files changed, 943 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5-v3.js create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5.js create mode 100644 performance-testing/uid2-operator/start-scenario-5-v3.sh create mode 100644 performance-testing/uid2-operator/start-scenario-5.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5-v3.js new file mode 100644 index 0000000..6bd2977 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5-v3.js @@ -0,0 +1,462 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(10000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(10000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5.js new file mode 100644 index 0000000..3a733c8 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-5.js @@ -0,0 +1,463 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(10000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(10000);; + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + "email": [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let emails = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(emails); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-5-v3.sh b/performance-testing/uid2-operator/start-scenario-5-v3.sh new file mode 100644 index 0000000..82b1fea --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-5-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-5-v3.js $COMMENT diff --git a/performance-testing/uid2-operator/start-scenario-5.sh b/performance-testing/uid2-operator/start-scenario-5.sh new file mode 100644 index 0000000..96bb319 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-5.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-5.js $COMMENT From 613c2f82e6bfdb4e78dc2c6af2b862ac899913c4 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 4 Mar 2026 10:52:03 +1100 Subject: [PATCH 16/27] add scenario 6 --- ...erate-refresh-identitymap-scenario-6-v3.js | 484 ++++++++++++++++++ .../uid2-operator/start-scenario-6-v3.sh | 9 + 2 files changed, 493 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-6-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-6-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-6-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-6-v3.js new file mode 100644 index 0000000..7d12191 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-6-v3.js @@ -0,0 +1,484 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + },/* + keySharingWarmup: { + executor: 'ramping-vus', + exec: 'keySharing', + stages: [ + { duration: warmUpTime, target: keySharingVUs} + ], + gracefulRampDown: '0s', + },*/ + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },/* + keySharing:{ + executor: 'constant-vus', + exec: 'keySharing', + vus: keySharingVUs, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + },*/ + /*identityMapLargeBatchSequential: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 1, + duration: '300s', + gracefulStop: '0s', + startTime: '970s', + }, + identityMapLargeBatch: { + executor: 'constant-vus', + exec: 'identityMapLargeBatch', + vus: 16, + duration: '300s', + gracefulStop: '0s', + startTime: '1280s', + }, + identityBuckets: { + executor: 'constant-vus', + exec: 'identityBuckets', + vus: 2, + duration: '300s', + gracefulStop: '0s', + startTime: '1590s', + },*/ + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(); + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapLargeBatch(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(); + } + + var requestBody = data.identityMap.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export function identityBuckets(data) { + var requestData = data.identityBuckets.requestData; + var elementToUse = selectRequestData(requestData); + + var bucketData = { + endpoint: data.identityBuckets.endpoint, + requestBody: elementToUse.requestBody, + } + execute(bucketData, true); +} + +export async function keySharing(data) { + const endpoint = '/v2/key/sharing'; + if (data.keySharing == null) { + var newData = await generateKeySharingRequestWithTime(); + data.keySharing = newData; + } else if (data.keySharing.time < (Date.now() - 45000)) { + data.keySharing = await generateKeySharingRequestWithTime(); + } + + var requestBody = data.keySharing.requestBody; + var keySharingData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(keySharingData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +// v3 supports mixed identity types in a single request. Each request contains +// 2,500 of each type: email, email_hash, phone, phone_hash (10,000 total). +async function generateIdentityMapRequest() { + const randomSuffix = Math.floor(Math.random() * 1_000_000_001); + const areaCode = ((randomSuffix % 800) + 200).toString(); + const countPerType = 2500; + + const emails = []; + const emailHashes = []; + const phones = []; + const phoneHashes = []; + + for (let i = 0; i < countPerType; i++) { + const email = `test${randomSuffix}${i}@example.com`; + emails.push(email); + emailHashes.push(await sha256Base64(email)); + + const phone = `+1${areaCode}555${i.toString().padStart(4, '0')}`; + phones.push(phone); + phoneHashes.push(await sha256Base64(phone)); + } + + return { + email: emails, + email_hash: emailHashes, + phone: phones, + phone_hash: phoneHashes, + }; +} + +async function sha256Base64(str) { + const data = stringToUint8Array(str); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + return encoding.b64encode(hashBuffer); +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime() { + let data = await generateIdentityMapRequest(); + return await generateRequestWithTime(data); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-6-v3.sh b/performance-testing/uid2-operator/start-scenario-6-v3.sh new file mode 100644 index 0000000..edc30e2 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-6-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-6-v3.js $COMMENT From bc54c461ea849d5352ba936408e5b7c54b039ee4 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 4 Mar 2026 16:07:53 +1100 Subject: [PATCH 17/27] add scenario 7 --- ...generate-refresh-identitymap-scenario-7.js | 433 ++++++++++++++++++ .../uid2-operator/start-scenario-7.sh | 9 + 2 files changed, 442 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-7.js create mode 100644 performance-testing/uid2-operator/start-scenario-7.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-7.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-7.js new file mode 100644 index 0000000..f8797ae --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-7.js @@ -0,0 +1,433 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const vus = 50; +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; +const identityMapRPS = 1500; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapV2Warmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMapV2', + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + }, + identityMapV3Warmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMapV3', + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMapV2: { + executor: 'constant-arrival-rate', + exec: 'identityMapV2', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMapV3: { + executor: 'constant-arrival-rate', + exec: 'identityMapV3', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 1000, + maxVUs: 1500, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMapV2: null, + identityMapV3: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMapV2(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMapV2 == null) || (data.identityMapV2.time < (Date.now() - 45000))) { + data.identityMapV2 = await generateIdentityMapV2RequestWithTime(5000); + } + + var requestBody = data.identityMapV2.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +export async function identityMapV3(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMapV3 == null) || (data.identityMapV3.time < (Date.now() - 45000))) { + data.identityMapV3 = await generateIdentityMapV3RequestWithTime(5000); + } + + var requestBody = data.identityMapV3.requestBody; + var identityData = { + endpoint: endpoint, + requestBody: requestBody, + } + execute(identityData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapV2Request(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function generateIdentityMapV3Request(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapV2RequestWithTime(emailCount) { + let data = generateIdentityMapV2Request(emailCount); + return await generateRequestWithTime(data); +} + +async function generateIdentityMapV3RequestWithTime(emailCount) { + let data = generateIdentityMapV3Request(emailCount); + return await generateRequestWithTime(data); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-7.sh b/performance-testing/uid2-operator/start-scenario-7.sh new file mode 100644 index 0000000..c2064e1 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-7.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-7.js $COMMENT From 0e12ce46f6237d345c0fdffa298251ea90fc71b0 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Wed, 4 Mar 2026 22:16:31 +1100 Subject: [PATCH 18/27] add scenario 8 --- ...erate-refresh-identitymap-scenario-8-v3.js | 388 +++++++++++++++++ ...generate-refresh-identitymap-scenario-8.js | 389 ++++++++++++++++++ .../uid2-operator/start-scenario-8-v3.sh | 9 + .../uid2-operator/start-scenario-8.sh | 9 + 4 files changed, 795 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8-v3.js create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8.js create mode 100644 performance-testing/uid2-operator/start-scenario-8-v3.sh create mode 100644 performance-testing/uid2-operator/start-scenario-8.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8-v3.js new file mode 100644 index 0000000..0a1c177 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8-v3.js @@ -0,0 +1,388 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Each identity map iteration fires CONCURRENT_REQUESTS requests simultaneously +// via http.batch(). The arrival rate is set so that: +// identityMapIterationsPerSecond × CONCURRENT_REQUESTS ≈ scenario 2's 1500 RPS +const CONCURRENT_REQUESTS = 10; +const identityMapIterationsPerSecond = 150; // 150 × 10 = 1500 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + stages: [ + { duration: warmUpTime, target: identityMapIterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapIterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v3/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000); + } + + const requestBody = data.identityMap.requestBody; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS; i++) { + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8.js new file mode 100644 index 0000000..68902b1 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-8.js @@ -0,0 +1,389 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Each identity map iteration fires CONCURRENT_REQUESTS requests simultaneously +// via http.batch(). The arrival rate is set so that: +// identityMapIterationsPerSecond × CONCURRENT_REQUESTS ≈ scenario 2's 1500 RPS +const CONCURRENT_REQUESTS = 10; +const identityMapIterationsPerSecond = 150; // 150 × 10 = 1500 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + stages: [ + { duration: warmUpTime, target: identityMapIterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapIterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + identityMap: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap(data) { + const endpoint = '/v2/identity/map'; + if ((data.identityMap == null) || (data.identityMap.time < (Date.now() - 45000))) { + data.identityMap = await generateIdentityMapRequestWithTime(5000); + } + + const requestBody = data.identityMap.requestBody; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS; i++) { + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +async function generateKeySharingRequestWithTime() { + let requestData = { }; + return await generateRequestWithTime(requestData); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-8-v3.sh b/performance-testing/uid2-operator/start-scenario-8-v3.sh new file mode 100644 index 0000000..6a4b43d --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-8-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-8-v3.js $COMMENT diff --git a/performance-testing/uid2-operator/start-scenario-8.sh b/performance-testing/uid2-operator/start-scenario-8.sh new file mode 100644 index 0000000..3629755 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-8.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-8.js $COMMENT From 3d85077b5ed8c4f4016191bb9611a1619ceadf71 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 11:06:35 +1100 Subject: [PATCH 19/27] add scenario 9 --- ...erate-refresh-identitymap-scenario-9-v3.js | 373 ++++++++++++++++++ .../uid2-operator/start-scenario-9-v3.sh | 9 + 2 files changed, 382 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-9-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9-v3.js new file mode 100644 index 0000000..e5f1354 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9-v3.js @@ -0,0 +1,373 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Low RPS for identity map to mimic real prod traffic. +// DIIs are generated fresh on every request (no 45s caching) so each call +// exercises a unique set of identifiers. +const identityMapRPS = 3; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap() { + // No caching: generate a fresh encrypted request with unique DIIs on every call. + const requestData = await generateIdentityMapRequestWithTime(5000); + + var identityMapData = { + endpoint: '/v3/identity/map', + requestBody: requestData.requestBody, + } + + execute(identityMapData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-9-v3.sh b/performance-testing/uid2-operator/start-scenario-9-v3.sh new file mode 100644 index 0000000..39c04aa --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-9-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-9-v3.js $COMMENT From 78641eea7bd14896fa454b70a7674b1c5cc91916 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 12:31:44 +1100 Subject: [PATCH 20/27] add scenario 10 --- ...rate-refresh-identitymap-scenario-10-v3.js | 380 ++++++++++++++++++ .../uid2-operator/start-scenario-10-v3.sh | 9 + 2 files changed, 389 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-10-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10-v3.js new file mode 100644 index 0000000..c6b5d24 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10-v3.js @@ -0,0 +1,380 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Each identity map iteration fires CONCURRENT_REQUESTS simultaneous requests +// via http.batch(), each with a freshly generated set of unique DIIs. +// identityMapIterationsPerSecond × CONCURRENT_REQUESTS = total HTTP requests/s +const CONCURRENT_REQUESTS = 3; +const identityMapIterationsPerSecond = 1; // 1 × 3 = 3 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + stages: [ + { duration: warmUpTime, target: identityMapIterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapIterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap() { + const endpoint = '/v3/identity/map'; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + // Generate a separate encrypted request body with unique DIIs for each + // concurrent request — no caching, no shared bodies between batch slots. + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS; i++) { + const requestData = await generateIdentityMapRequestWithTime(10000); + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-10-v3.sh b/performance-testing/uid2-operator/start-scenario-10-v3.sh new file mode 100644 index 0000000..1eb8bcf --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-10-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-10-v3.js $COMMENT From 5c45a858565918e1fef76108440d7263f4a1d83e Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 14:38:53 +1100 Subject: [PATCH 21/27] add v2 scenario 10 --- ...enerate-refresh-identitymap-scenario-10.js | 381 ++++++++++++++++++ .../uid2-operator/start-scenario-10.sh | 9 + 2 files changed, 390 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10.js create mode 100644 performance-testing/uid2-operator/start-scenario-10.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10.js new file mode 100644 index 0000000..c1182c7 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-10.js @@ -0,0 +1,381 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Each identity map iteration fires CONCURRENT_REQUESTS simultaneous requests +// via http.batch(), each with a freshly generated set of unique DIIs. +// identityMapIterationsPerSecond × CONCURRENT_REQUESTS = total HTTP requests/s +const CONCURRENT_REQUESTS = 3; +const identityMapIterationsPerSecond = 1; // 1 × 3 = 3 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + stages: [ + { duration: warmUpTime, target: identityMapIterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapIterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap() { + const endpoint = '/v2/identity/map'; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + // Generate a separate encrypted request body with unique DIIs for each + // concurrent request — no caching, no shared bodies between batch slots. + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS; i++) { + const requestData = await generateIdentityMapRequestWithTime(10000); + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-10.sh b/performance-testing/uid2-operator/start-scenario-10.sh new file mode 100644 index 0000000..19a20c6 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-10.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-10.js $COMMENT From b24e900e5a5db30be9ccbadf5274444c15484499 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 15:37:52 +1100 Subject: [PATCH 22/27] add scenario 11 --- ...enerate-refresh-identitymap-scenario-11.js | 434 ++++++++++++++++++ .../uid2-operator/start-scenario-11.sh | 9 + 2 files changed, 443 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js create mode 100644 performance-testing/uid2-operator/start-scenario-11.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js new file mode 100644 index 0000000..b1d48d8 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js @@ -0,0 +1,434 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// /v2/identity/map: high-throughput, cached DIIs (45s window), 5k emails per request +const identityMapV2RPS = 5000; + +// /v3/identity/map: scenario-10 conditions — 3 concurrent requests per iteration, +// each with freshly generated unique DIIs, 10k emails per request, ~1 iteration/s +const CONCURRENT_REQUESTS_V3 = 3; +const identityMapV3IterationsPerSecond = 1; // 1 × 3 = 3 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapV2Warmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMapV2', + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + stages: [ + { duration: warmUpTime, target: identityMapV2RPS} + ], + }, + identityMapV3Warmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMapV3', + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + stages: [ + { duration: warmUpTime, target: identityMapV3IterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMapV2: { + executor: 'constant-arrival-rate', + exec: 'identityMapV2', + rate: identityMapV2RPS, + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMapV3: { + executor: 'constant-arrival-rate', + exec: 'identityMapV3', + rate: identityMapV3IterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMapV2() { + // No caching: generate a fresh encrypted request with unique DIIs on every call. + const requestData = await generateIdentityMapV2RequestWithTime(50); + + var identityMapData = { + endpoint: '/v2/identity/map', + requestBody: requestData.requestBody, + } + + execute(identityMapData, true); +} + +export async function identityMapV3() { + const endpoint = '/v3/identity/map'; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + // Generate a separate encrypted request body with unique DIIs for each + // concurrent request — no caching, no shared bodies between batch slots. + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS_V3; i++) { + const requestData = await generateIdentityMapV3RequestWithTime(10000); + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapV2Request(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function generateIdentityMapV3Request(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapV2RequestWithTime(emailCount) { + let data = generateIdentityMapV2Request(emailCount); + return await generateRequestWithTime(data); +} + +async function generateIdentityMapV3RequestWithTime(emailCount) { + let data = generateIdentityMapV3Request(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-11.sh b/performance-testing/uid2-operator/start-scenario-11.sh new file mode 100644 index 0000000..c534adb --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-11.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-11.js $COMMENT From 23ea5f52ddc464c3a62da1dd2ac45149dda63200 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 15:40:40 +1100 Subject: [PATCH 23/27] add large batch chance for /v2/identity/map --- .../k6-token-generate-refresh-identitymap-scenario-11.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js index b1d48d8..4b73905 100644 --- a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11.js @@ -189,7 +189,10 @@ export function tokenRefresh(data) { export async function identityMapV2() { // No caching: generate a fresh encrypted request with unique DIIs on every call. - const requestData = await generateIdentityMapV2RequestWithTime(50); + // 2% of requests use a large 5000-email batch; the rest use 50 emails. + const largeBatchChance = 0.02; + const emailCount = Math.random() < largeBatchChance ? 5000 : 50; + const requestData = await generateIdentityMapV2RequestWithTime(emailCount); var identityMapData = { endpoint: '/v2/identity/map', From f42716c9cfc7730fa064aabba0cc17d7372c8f7f Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 21:02:01 +1100 Subject: [PATCH 24/27] add scenario 12 --- ...enerate-refresh-identitymap-scenario-12.js | 382 ++++++++++++++++++ .../uid2-operator/start-scenario-12.sh | 9 + 2 files changed, 391 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12.js create mode 100644 performance-testing/uid2-operator/start-scenario-12.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12.js new file mode 100644 index 0000000..a106d76 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12.js @@ -0,0 +1,382 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Like scenario 8: 10 concurrent requests per iteration via http.batch(). +// Unlike scenario 8: each batch slot gets a freshly generated body with unique DIIs — +// no 45s caching, no shared body across slots. +// identityMapIterationsPerSecond × CONCURRENT_REQUESTS ≈ scenario 2's 1500 RPS +const CONCURRENT_REQUESTS = 10; +const identityMapIterationsPerSecond = 150; // 150 × 10 = 1500 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + stages: [ + { duration: warmUpTime, target: identityMapIterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapIterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap() { + const endpoint = '/v2/identity/map'; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + // Generate a separate encrypted request body with unique DIIs for each + // concurrent request — no caching, no shared bodies between batch slots. + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS; i++) { + const requestData = await generateIdentityMapRequestWithTime(5000); + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-12.sh b/performance-testing/uid2-operator/start-scenario-12.sh new file mode 100644 index 0000000..65a47a8 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-12.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-12.js $COMMENT From 1f10c2fe5c346595803320520acaee8ef7060c34 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Thu, 5 Mar 2026 21:31:57 +1100 Subject: [PATCH 25/27] add v3 scenario 12 --- ...rate-refresh-identitymap-scenario-12-v3.js | 381 ++++++++++++++++++ .../uid2-operator/start-scenario-12-v3.sh | 9 + 2 files changed, 390 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12-v3.js create mode 100644 performance-testing/uid2-operator/start-scenario-12-v3.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12-v3.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12-v3.js new file mode 100644 index 0000000..bd1162e --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-12-v3.js @@ -0,0 +1,381 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Like scenario 8-v3: 10 concurrent requests per iteration via http.batch(). +// Unlike scenario 8-v3: each batch slot gets a freshly generated body with unique DIIs — +// no 45s caching, no shared body across slots. +// identityMapIterationsPerSecond × CONCURRENT_REQUESTS ≈ scenario 2's 1500 RPS +const CONCURRENT_REQUESTS = 10; +const identityMapIterationsPerSecond = 150; // 150 × 10 = 1500 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + stages: [ + { duration: warmUpTime, target: identityMapIterationsPerSecond} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapIterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap() { + const endpoint = '/v3/identity/map'; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + // Generate a separate encrypted request body with unique DIIs for each + // concurrent request — no caching, no shared bodies between batch slots. + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS; i++) { + const requestData = await generateIdentityMapRequestWithTime(5000); + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-12-v3.sh b/performance-testing/uid2-operator/start-scenario-12-v3.sh new file mode 100644 index 0000000..2cc3ad4 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-12-v3.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-12-v3.js $COMMENT From 2e6fe57979fc433307a46a46dd4cf6913feb9681 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 6 Mar 2026 20:30:50 +1100 Subject: [PATCH 26/27] add scenario 11 with no v3 warmup --- ...sh-identitymap-scenario-11-no-v3-warmup.js | 430 ++++++++++++++++++ .../start-scenario-11-no-v3-warmup.sh | 9 + 2 files changed, 439 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11-no-v3-warmup.js create mode 100644 performance-testing/uid2-operator/start-scenario-11-no-v3-warmup.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11-no-v3-warmup.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11-no-v3-warmup.js new file mode 100644 index 0000000..2c1a6e5 --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-11-no-v3-warmup.js @@ -0,0 +1,430 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// /v2/identity/map: high-throughput, fresh DIIs, mixed batch sizes +const identityMapV2RPS = 5000; + +// /v3/identity/map: scenario-10 conditions — 3 concurrent requests per iteration, +// each with freshly generated unique DIIs, 10k emails per request, ~1 iteration/s. +// NOTE: unlike scenario 11, /v3/identity/map has NO warmup phase — it starts cold +// at t=10m into the test. This simulates prod behaviour where v3 only receives +// burst traffic and the v3-specific JVM code paths are never JIT-compiled. +const CONCURRENT_REQUESTS_V3 = 3; +const identityMapV3IterationsPerSecond = 1; // 1 × 3 = 3 total HTTP requests/s + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios — intentionally excludes identityMapV3Warmup + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapV2Warmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMapV2', + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + stages: [ + { duration: warmUpTime, target: identityMapV2RPS} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMapV2: { + executor: 'constant-arrival-rate', + exec: 'identityMapV2', + rate: identityMapV2RPS, + timeUnit: '1s', + preAllocatedVUs: 100, + maxVUs: 200, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMapV3: { + executor: 'constant-arrival-rate', + exec: 'identityMapV3', + rate: identityMapV3IterationsPerSecond, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMapV2() { + // No caching: generate a fresh encrypted request with unique DIIs on every call. + // 2% of requests use a large 5000-email batch; the rest use 50 emails. + const largeBatchChance = 0.02; + const emailCount = Math.random() < largeBatchChance ? 5000 : 50; + const requestData = await generateIdentityMapV2RequestWithTime(emailCount); + + var identityMapData = { + endpoint: '/v2/identity/map', + requestBody: requestData.requestBody, + } + + execute(identityMapData, true); +} + +export async function identityMapV3() { + const endpoint = '/v3/identity/map'; + const authOptions = { headers: { 'Authorization': `Bearer ${clientKey}` } }; + + // Generate a separate encrypted request body with unique DIIs for each + // concurrent request — no caching, no shared bodies between batch slots. + const batchRequests = []; + for (let i = 0; i < CONCURRENT_REQUESTS_V3; i++) { + const requestData = await generateIdentityMapV3RequestWithTime(10000); + batchRequests.push(['POST', `${baseUrl}${endpoint}`, requestData.requestBody, authOptions]); + } + + const responses = http.batch(batchRequests); + for (const r of responses) { + check(r, { 'status is 200': res => res.status === 200 }); + } +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapV2Request(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function generateIdentityMapV3Request(emailCount) { + var data = { + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapV2RequestWithTime(emailCount) { + let data = generateIdentityMapV2Request(emailCount); + return await generateRequestWithTime(data); +} + +async function generateIdentityMapV3RequestWithTime(emailCount) { + let data = generateIdentityMapV3Request(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-11-no-v3-warmup.sh b/performance-testing/uid2-operator/start-scenario-11-no-v3-warmup.sh new file mode 100644 index 0000000..0cf74d9 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-11-no-v3-warmup.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-11-no-v3-warmup.js $COMMENT From 40a9cb55625935fff8b6e86d2b23e8dfcd518c47 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Sun, 8 Mar 2026 17:18:05 +1100 Subject: [PATCH 27/27] add v2 scenario 9 --- ...generate-refresh-identitymap-scenario-9.js | 374 ++++++++++++++++++ .../uid2-operator/start-scenario-9.sh | 9 + 2 files changed, 383 insertions(+) create mode 100644 performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9.js create mode 100644 performance-testing/uid2-operator/start-scenario-9.sh diff --git a/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9.js b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9.js new file mode 100644 index 0000000..19cf7dd --- /dev/null +++ b/performance-testing/uid2-operator/k6-token-generate-refresh-identitymap-scenario-9.js @@ -0,0 +1,374 @@ +import encoding from 'k6/encoding'; +import { check } from 'k6'; +import http from 'k6/http'; + +const baseUrl = __ENV.OPERATOR_URL; +const clientSecret = __ENV.CLIENT_SECRET; +const clientKey = __ENV.CLIENT_KEY; + +const generateRPS = 25000; +const refreshRPS = 25000; + +// Low RPS for identity map to mimic real prod traffic. +// DIIs are generated fresh on every request (no 45s caching) so each call +// exercises a unique set of identifiers. +const identityMapRPS = 3; + +const warmUpTime = '10m' +const testDuration = '20m' + +export const options = { + insecureSkipTLSVerify: true, + noConnectionReuse: false, + scenarios: { + // Warmup scenarios + tokenGenerateWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenGenerate', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: generateRPS} + ], + }, + tokenRefreshWarmup: { + executor: 'ramping-arrival-rate', + exec: 'tokenRefresh', + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + stages: [ + { duration: warmUpTime, target: refreshRPS} + ], + }, + identityMapWarmup: { + executor: 'ramping-arrival-rate', + exec: 'identityMap', + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + stages: [ + { duration: warmUpTime, target: identityMapRPS} + ], + }, + // Actual testing scenarios + tokenGenerate: { + executor: 'constant-arrival-rate', + exec: 'tokenGenerate', + rate: generateRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + tokenRefresh: { + executor: 'constant-arrival-rate', + exec: 'tokenRefresh', + rate: refreshRPS, + timeUnit: '1s', + preAllocatedVUs: 200, + maxVUs: 400, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + identityMap: { + executor: 'constant-arrival-rate', + exec: 'identityMap', + rate: identityMapRPS, + timeUnit: '1s', + preAllocatedVUs: 5, + maxVUs: 10, + duration: testDuration, + gracefulStop: '0s', + startTime: warmUpTime, + }, + }, + // So we get count in the summary, to demonstrate different metrics are different + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)', 'count'], + thresholds: { + // Intentionally empty. We'll programatically define our bogus + // thresholds (to generate the sub-metrics) below. In your real-world + // load test, you can add any real threshoulds you want here. + } +}; + +// https://community.k6.io/t/multiple-scenarios-metrics-per-each/1314/3 +for (let key in options.scenarios) { + // Each scenario automaticall tags the metrics it generates with its own name + let thresholdName = `http_req_duration{scenario:${key}}`; + // Check to prevent us from overwriting a threshold that already exists + if (!options.thresholds[thresholdName]) { + options.thresholds[thresholdName] = []; + } + // 'max>=0' is a bogus condition that will always be fulfilled + options.thresholds[thresholdName].push('max>=0'); +} + +export async function setup() { + var token = await generateRefreshRequest(); + return { + tokenGenerate: null, + refreshToken: token + }; + + async function generateRefreshRequest() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let request = await createReq( {'optout_check': 1, 'email': `test${randomSuffix}@example.com`}); + var requestData = { + endpoint: '/v2/token/generate', + requestBody: request, + } + let response = await send(requestData, clientKey); + let decrypt = await decryptEnvelope(response.body, clientSecret) + return decrypt.body.refresh_token; + }; +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), + } +} + +// Scenarios +export async function tokenGenerate(data) { + const endpoint = '/v2/token/generate'; + if (data.tokenGenerate == null) { + var newData = await generateTokenGenerateRequestWithTime(); + data.tokenGenerate = newData; + } else if (data.tokenGenerate.time < (Date.now() - 45000)) { + data.tokenGenerate = await generateTokenGenerateRequestWithTime(); + } + + var requestBody = data.tokenGenerate.requestBody; + var tokenGenerateData = { + endpoint: endpoint, + requestBody: requestBody, + } + + execute(tokenGenerateData, true); +} + +export function tokenRefresh(data) { + var requestBody = data.refreshToken; + var refreshData = { + endpoint: '/v2/token/refresh', + requestBody: requestBody + } + + execute(refreshData, false); +} + +export async function identityMap() { + // No caching: generate a fresh encrypted request with unique DIIs on every call. + const requestData = await generateIdentityMapRequestWithTime(5000); + + var identityMapData = { + endpoint: '/v2/identity/map', + requestBody: requestData.requestBody, + } + + execute(identityMapData, true); +} + +// Helpers +async function createReqWithTimestamp(timestampArr, obj) { + var envelope = getEnvelopeWithTimestamp(timestampArr, obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +} + +function generateIdentityMapRequest(emailCount) { + var data = { + 'optout_check': 1, + 'email': [] + }; + + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + for (var i = 0; i < emailCount; ++i) { + data.email.push(`test${randomSuffix}${i}@example.com`); + } + + return data; +} + +function send(data, auth) { + var options = {}; + if (auth) { + options.headers = { + 'Authorization': `Bearer ${clientKey}` + }; + } + + return http.post(`${baseUrl}${data.endpoint}`, data.requestBody, options); +} + +function execute(data, auth) { + var response = send(data, auth); + + check(response, { + 'status is 200': r => r.status === 200, + }); +} + +async function encryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const ciphertext = new Uint8Array(await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + envelope + )); + + const result = new Uint8Array(+(1 + iv.length + ciphertext.length)); + + // The version of the envelope format. + result[0] = 1; + + result.set(iv, 1); + + // The tag is at the end of ciphertext. + result.set(ciphertext, 1 + iv.length); + + return result; +} + +async function decryptEnvelope(envelope, clientSecret) { + const rawKey = encoding.b64decode(clientSecret); + const rawData = encoding.b64decode(envelope); + const key = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", true, [ + "encrypt", + "decrypt", + ]); + const length = rawData.byteLength; + const iv = rawData.slice(0, 12); + + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + tagLength: 128 + }, + key, + rawData.slice(12) + ); + + + const decryptedResponse = String.fromCharCode.apply(String, new Uint8Array(decrypted.slice(16))); + const response = JSON.parse(decryptedResponse); + + return response; +} + +function getEnvelopeWithTimestamp(timestampArray, obj) { + var randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + + var payload = stringToUint8Array(JSON.stringify(obj)); + + var envelope = new Uint8Array(timestampArray.length + randomBytes.length + payload.length); + envelope.set(timestampArray); + envelope.set(randomBytes, timestampArray.length); + envelope.set(payload, timestampArray.length + randomBytes.length); + + return envelope; + +} +function getEnvelope(obj) { + var timestampArr = new Uint8Array(getTimestamp()); + return getEnvelopeWithTimestamp(timestampArr, obj); +} + +function getTimestamp() { + const now = Date.now(); + return getTimestampFromTime(now); +} + +function getTimestampFromTime(time) { + const res = new ArrayBuffer(8); + const { hi, lo } = Get32BitPartsBE(time); + const view = new DataView(res); + view.setUint32(0, hi, false); + view.setUint32(4, lo, false); + return res; +} + +// http://anuchandy.blogspot.com/2015/03/javascript-how-to-extract-lower-32-bit.html +function Get32BitPartsBE(bigNumber) { + if (bigNumber > 9007199254740991) { + // Max int that JavaScript can represent is 2^53. + throw new Error('The 64-bit value is too big to be represented in JS :' + bigNumber); + } + + var bigNumberAsBinaryStr = bigNumber.toString(2); + // Convert the above binary str to 64 bit (actually 52 bit will work) by padding zeros in the left + var bigNumberAsBinaryStr2 = ''; + for (var i = 0; i < 64 - bigNumberAsBinaryStr.length; i++) { + bigNumberAsBinaryStr2 += '0'; + }; + + bigNumberAsBinaryStr2 += bigNumberAsBinaryStr; + + return { + hi: parseInt(bigNumberAsBinaryStr2.substring(0, 32), 2), + lo: parseInt(bigNumberAsBinaryStr2.substring(32), 2), + }; +} + +function stringToUint8Array(str) { + const buffer = new ArrayBuffer(str.length); + const view = new Uint8Array(buffer); + for (var i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i); + } + return view; +} + +async function createReq(obj) { + var envelope = getEnvelope(obj); + return encoding.b64encode((await encryptEnvelope(envelope, clientSecret)).buffer); +}; + +async function generateRequestWithTime(obj) { + var time = Date.now(); + var timestampArr = new Uint8Array(getTimestampFromTime(time)); + var requestBody = await createReqWithTimestamp(timestampArr, obj); + var element = { + time: time, + requestBody: requestBody + }; + + return element; +} + +async function generateTokenGenerateRequestWithTime() { + let randomSuffix = Math.floor(Math.random() * 1_000_000_001); + let requestData = { 'optout_check': 1, 'email': `test${randomSuffix}@example.com` }; + return await generateRequestWithTime(requestData); +} + +async function generateIdentityMapRequestWithTime(emailCount) { + let data = generateIdentityMapRequest(emailCount); + return await generateRequestWithTime(data); +} + +const generateSinceTimestampStr = () => { + var date = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 /* 2 days ago */); + var year = date.getFullYear(); + var month = (date.getMonth() + 1).toString().padStart(2, '0'); + var day = date.getDate().toString().padStart(2, '0'); + + return `${year}-${month}-${day}T00:00:00`; +}; diff --git a/performance-testing/uid2-operator/start-scenario-9.sh b/performance-testing/uid2-operator/start-scenario-9.sh new file mode 100644 index 0000000..859ca79 --- /dev/null +++ b/performance-testing/uid2-operator/start-scenario-9.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +COMMENT=$1 + +if [ "$#" -ne 1 ]; then + COMMENT=$( date '+%F_%H:%M:%S' ) +fi + +./start-named-test.sh k6-token-generate-refresh-identitymap-scenario-9.js $COMMENT