├── .nvmrc ├── .env.example ├── .eslintrc.js ├── test.sh ├── .gitignore ├── lib ├── timer.js ├── delivery.js └── content.js ├── run.sh ├── package.json ├── README.md ├── .circleci └── config.yml └── bin ├── test-entry-creation.js └── test-entry-update.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v10 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CMA_TOKEN= 2 | CDA_TOKEN= 3 | 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@contentful/backend' 3 | }; 4 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | namespace=CtfPerf 2 | 3 | for filename in bin/* 4 | do 5 | node $filename 6 | done 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.nyc_output 3 | built 4 | /coverage 5 | coverage.lcov 6 | /.envrc 7 | TAGS 8 | .env 9 | -------------------------------------------------------------------------------- /lib/timer.js: -------------------------------------------------------------------------------- 1 | module.exports = timer; 2 | 3 | function timer () { 4 | const startedAt = process.hrtime(); 5 | 6 | return () => { 7 | const endedAt = process.hrtime(startedAt); 8 | return endedAt[0] * 1000 + endedAt[1] / 1e6; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | namespace=CtfPerf 2 | 3 | for filename in bin/* 4 | do 5 | metricName=$(basename $filename | cut -f 1 -d '.' | sed -r 's/(^|_|-)([a-z])/\U\2/g') 6 | aws cloudwatch put-metric-data --metric-name $metricName --namespace $namespace --value $(node $filename) --unit Milliseconds --timestamp $(date -u +"%Y-%m-%dT%H:%M:%SZ") --region us-east-1 7 | sleep 10 8 | done 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-perf-test", 3 | "version": "0.0.1", 4 | "description": "Synth test for time to publish", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node run.js", 8 | "lint": "eslint './**/*.js'" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dotenv": "^8.0.0", 14 | "request": "^2.88.0", 15 | "request-promise-native": "^1.0.7" 16 | }, 17 | "devDependencies": { 18 | "@contentful/eslint-config-backend": "^6.0.1", 19 | "eslint": "^4.19.1", 20 | "eslint-plugin-import": "^2.18.2", 21 | "eslint-plugin-mocha": "^6.0.0", 22 | "eslint-plugin-node": "^9.1.0", 23 | "eslint-plugin-promise": "^4.2.1", 24 | "eslint-plugin-standard": "^4.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-perf-test 2 | 3 | This repository contains several performance measurement tools for Contentful APIs. The goal is to record how fast Contentful platform can perform a common userland action, such as following tests implemented; 4 | 5 | * Publishing a new entry 6 | * Publishing an updated entry 7 | 8 | # Usage 9 | 10 | Prepare following environment variables in your system; 11 | 12 | ``` 13 | CDA_TOKEN="" # required 14 | CMA_TOKEN="" # required 15 | SPACE_ID= # required 16 | ENTRY_ID= # required only for update test 17 | ``` 18 | 19 | And now you can run the tests individually, calling their path; 20 | 21 | ```bash 22 | $ node bin/test-entry-creation.js 23 | ``` 24 | 25 | Or you can run them all at once; 26 | 27 | ```bash 28 | $ ./run.sh 29 | ``` 30 | 31 | # Cache Purging 32 | 33 | Contentful currently allows one publish per every 10 seconds. If more than one request is sent within that timeframe, the requests will be debounced, and delayed for up to 2 minutes. This limitation applies to all publish requests for same space, therefore; 34 | 35 | * Tests in same space should run serially 36 | * Test runner (`./test.sh`) should have 10 seconds delay in between every sub execution. 37 | -------------------------------------------------------------------------------- /lib/delivery.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise-native'); 2 | 3 | module.exports = { 4 | getPublishedRevision, 5 | isContentDeliveredYet, 6 | waitUntilContentIsDelivered 7 | }; 8 | 9 | async function waitUntilContentIsDelivered (options) { 10 | while (true) { 11 | if (await isContentDeliveredYet(options)) { 12 | break; 13 | } 14 | } 15 | } 16 | 17 | // Promise 18 | async function isContentDeliveredYet ({ 19 | cdaToken, 20 | spaceId, 21 | entryId, 22 | expectedRevision 23 | }) { 24 | let publishedRevision; 25 | try { 26 | publishedRevision = await getPublishedRevision({ 27 | cdaToken, 28 | spaceId, 29 | entryId 30 | }); 31 | } catch (err) { 32 | if (err.statusCode === 404) { 33 | return false; 34 | } 35 | 36 | throw err; 37 | } 38 | 39 | return publishedRevision === expectedRevision; 40 | } 41 | 42 | // Promise 43 | async function getPublishedRevision ({ cdaToken, spaceId, entryId }) { 44 | const options = { 45 | method: 'GET', 46 | url: `https://cdn.contentful.com/spaces/${spaceId}/entries/${entryId}`, 47 | headers: { 48 | Authorization: `Bearer ${cdaToken}`, 49 | 'Content-Type': 'application/json' 50 | } 51 | }; 52 | 53 | const raw = await request(options); 54 | const parsed = JSON.parse(raw); 55 | 56 | return parsed.sys.revision; 57 | } 58 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | benchmark: 4 | docker: 5 | - image: circleci/node:10.0.0 6 | steps: 7 | - checkout 8 | - run: 9 | name: Setup docker 10 | command: | 11 | docker --version 12 | - run: 13 | name: Setup circle 14 | command: | 15 | circleci version 16 | - run: 17 | name: Install Node deps 18 | command: | 19 | npm install 20 | - run: 21 | name: Install AWS CLI 22 | command: | 23 | sudo apt-get update && sudo apt-get install -y awscli 24 | - run: 25 | name: Perform benchmark 26 | command: | 27 | ./run.sh 28 | test: 29 | docker: 30 | - image: circleci/node:10.0.0 31 | steps: 32 | - checkout 33 | - run: 34 | name: Setup docker 35 | command: | 36 | docker --version 37 | - run: 38 | name: Setup circle 39 | command: | 40 | circleci version 41 | - run: 42 | name: Install Node deps 43 | command: | 44 | npm install 45 | - run: 46 | name: Perform test 47 | command: | 48 | ./test.sh 49 | 50 | workflows: 51 | version: 2 52 | test: 53 | jobs: 54 | - test 55 | 56 | nightly: 57 | triggers: 58 | - schedule: 59 | cron: "0 * * * *" 60 | filters: 61 | branches: 62 | only: 63 | - master 64 | jobs: 65 | - benchmark 66 | -------------------------------------------------------------------------------- /bin/test-entry-creation.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const startTimer = require('../lib/timer'); 4 | const { 5 | createEntry, 6 | publishEntry, 7 | deleteEntry, 8 | unpublishEntry 9 | } = require('../lib/content'); 10 | const { waitUntilContentIsDelivered } = require('../lib/delivery'); 11 | 12 | const cmaToken = process.env.CMA_TOKEN; 13 | const spaceId = process.env.SPACE_ID; 14 | const cdaToken = process.env.CDA_TOKEN; 15 | 16 | require.main === module && run({ cmaToken, cdaToken, spaceId }); 17 | 18 | module.exports = { 19 | run 20 | }; 21 | 22 | async function run ({ cmaToken, cdaToken, spaceId }) { 23 | // Create a new entry 24 | // Publish it 25 | // Send requests to CDA until published version is delivered 26 | // Print out the time that took 27 | const [entryId, entryVersion] = await createEntry({ 28 | cmaToken, 29 | spaceId, 30 | contentTypeId: 'simpleContentType', 31 | populateContent: populateSimpleEntry 32 | }); 33 | 34 | const endTimer = startTimer(); 35 | 36 | await publishEntry({ cmaToken, spaceId, entryId, version: entryVersion }); 37 | await waitUntilContentIsDelivered({ 38 | entryId, 39 | expectedRevision: 1, 40 | spaceId, 41 | cdaToken 42 | }); 43 | 44 | console.log(endTimer()); 45 | 46 | await unpublishEntry({ cmaToken, spaceId, entryId, entryVersion }); 47 | await deleteEntry({ cmaToken, spaceId, entryId, entryVersion }); 48 | } 49 | 50 | function populateSimpleEntry () { 51 | return { 52 | fields: { 53 | title: { 54 | 'en-US': Date.now().toString(36) 55 | } 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /bin/test-entry-update.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | const startTimer = require('../lib/timer'); 4 | 5 | const { 6 | updateEntry, 7 | publishEntry, 8 | getLatestVersion 9 | } = require('../lib/content'); 10 | 11 | const { 12 | waitUntilContentIsDelivered, 13 | getPublishedRevision 14 | } = require('../lib/delivery'); 15 | 16 | const cmaToken = process.env.CMA_TOKEN; 17 | const spaceId = process.env.SPACE_ID; 18 | const cdaToken = process.env.CDA_TOKEN; 19 | const existingEntryId = process.env.ENTRY_ID; 20 | 21 | require.main === module && run({ cmaToken, cdaToken, spaceId }); 22 | 23 | module.exports = { 24 | run 25 | }; 26 | 27 | async function run ({ cmaToken, cdaToken, spaceId }) { 28 | const latestPublishedRevision = await getPublishedRevision({ 29 | cdaToken, 30 | spaceId, 31 | entryId: existingEntryId 32 | }); 33 | 34 | const latestVersion = await getLatestVersion({ 35 | cmaToken, 36 | spaceId, 37 | entryId: existingEntryId 38 | }); 39 | 40 | // Update existing entry, get updated version number 41 | // Publish it 42 | // Send requests to CDA until published version is delivered 43 | // Print out the time that took 44 | const updatedVersion = await updateEntry({ 45 | cmaToken, 46 | spaceId, 47 | version: latestVersion, 48 | entryId: existingEntryId, 49 | populateContent: populateSimpleEntry 50 | }); 51 | 52 | const endTimer = startTimer(); 53 | 54 | await publishEntry({ 55 | cmaToken, 56 | spaceId, 57 | entryId: existingEntryId, 58 | version: updatedVersion 59 | }); 60 | 61 | await waitUntilContentIsDelivered({ 62 | entryId: existingEntryId, 63 | expectedRevision: latestPublishedRevision + 1, 64 | spaceId, 65 | cdaToken 66 | }); 67 | 68 | console.log(endTimer()); 69 | } 70 | 71 | function populateSimpleEntry () { 72 | return { 73 | fields: { 74 | title: { 75 | 'en-US': Date.now().toString(36) 76 | } 77 | } 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /lib/content.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise-native'); 2 | 3 | module.exports = { 4 | createEntry, 5 | deleteEntry, 6 | getLatestVersion, 7 | unpublishEntry, 8 | updateEntry, 9 | publishEntry 10 | }; 11 | 12 | // Promise<[createdEntryId: int, createdEntryVersion: int]> 13 | async function createEntry ({ 14 | cmaToken, 15 | spaceId, 16 | contentTypeId, 17 | populateContent 18 | }) { 19 | const options = { 20 | method: 'POST', 21 | url: `https://api.contentful.com/spaces/${spaceId}/environments/master/entries`, 22 | token: cmaToken, 23 | headers: { 24 | Authorization: `Bearer ${cmaToken}`, 25 | 'Content-Type': 'application/json', 26 | 'X-Contentful-Content-Type': contentTypeId 27 | }, 28 | body: JSON.stringify(populateContent()) 29 | }; 30 | 31 | const raw = await request(options); 32 | const parsed = JSON.parse(raw); 33 | 34 | return [parsed.sys.id, parsed.sys.version]; 35 | } 36 | 37 | // Promise<> 38 | async function deleteEntry ({ cmaToken, spaceId, entryId, version }) { 39 | await request({ 40 | method: 'DELETE', 41 | url: `https://api.contentful.com/spaces/${spaceId}/environments/master/entries/${entryId}`, 42 | token: cmaToken, 43 | headers: { 44 | Authorization: `Bearer ${cmaToken}`, 45 | 'Content-Type': 'application/json', 46 | 'X-Contentful-Version': version 47 | } 48 | }); 49 | } 50 | 51 | // Promise 52 | async function updateEntry ({ 53 | cmaToken, 54 | spaceId, 55 | entryId, 56 | version, 57 | populateContent 58 | }) { 59 | const options = { 60 | method: 'PUT', 61 | url: `https://api.contentful.com/spaces/${spaceId}/environments/master/entries/${entryId}`, 62 | token: cmaToken, 63 | headers: { 64 | Authorization: `Bearer ${cmaToken}`, 65 | 'Content-Type': 'application/json', 66 | 'X-Contentful-Version': version 67 | }, 68 | body: JSON.stringify(populateContent()) 69 | }; 70 | 71 | const raw = await request(options); 72 | const parsed = JSON.parse(raw); 73 | 74 | return parsed.sys.version; 75 | } 76 | 77 | // Promise<> 78 | async function publishEntry ({ cmaToken, spaceId, entryId, version }) { 79 | await request({ 80 | method: 'PUT', 81 | url: `https://api.contentful.com/spaces/${spaceId}/environments/master/entries/${entryId}/published`, 82 | token: cmaToken, 83 | headers: { 84 | Authorization: `Bearer ${cmaToken}`, 85 | 'Content-Type': 'application/json', 86 | 'X-Contentful-Version': version 87 | } 88 | }); 89 | } 90 | 91 | // Promise<> 92 | async function unpublishEntry ({ cmaToken, spaceId, entryId, version }) { 93 | await request({ 94 | method: 'DELETE', 95 | url: `https://api.contentful.com/spaces/${spaceId}/environments/master/entries/${entryId}/published`, 96 | token: cmaToken, 97 | headers: { 98 | Authorization: `Bearer ${cmaToken}`, 99 | 'Content-Type': 'application/json', 100 | 'X-Contentful-Version': version 101 | } 102 | }); 103 | } 104 | 105 | // Promise 106 | async function getLatestVersion ({ cmaToken, spaceId, entryId }) { 107 | const options = { 108 | method: 'GET', 109 | url: `https://api.contentful.com/spaces/${spaceId}/entries/${entryId}`, 110 | headers: { 111 | Authorization: `Bearer ${cmaToken}`, 112 | 'Content-Type': 'application/json' 113 | } 114 | }; 115 | 116 | const raw = await request(options); 117 | const parsed = JSON.parse(raw); 118 | 119 | return parsed.sys.version; 120 | } 121 | --------------------------------------------------------------------------------