diff --git a/.github/workflows/build-preview.yml b/.github/workflows/build-preview.yml index ae0bb9f69..0a422a302 100644 --- a/.github/workflows/build-preview.yml +++ b/.github/workflows/build-preview.yml @@ -12,12 +12,31 @@ permissions: pages: write pull-requests: write id-token: write + checks: write concurrency: preview-${{ github.ref }} jobs: deploy-preview: runs-on: ubuntu-latest steps: - - name: Checkout 🛎️ + - name: Checkout Main Branch 🛎️ + uses: actions/checkout@v3 + with: + ref: main + - name: Install and Build Main Branch 🔧 + run: | + npm ci --include=dev + npm run build + npm run style + npm run shields + cp src/configs/config.aws.js src/config.js + - name: Capture main branch usage statistics + id: main-stats + run: | + MAIN_STATS=$(node scripts/stats.js -j) + echo "MAIN_STATS<> $GITHUB_ENV + echo -e "$MAIN_STATS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + - name: Checkout PR Branch 🛎️ uses: actions/checkout@v3 - name: Use Node.js 18.16.1 uses: actions/setup-node@v3 @@ -42,6 +61,30 @@ jobs: npm run style npm run shields cp src/configs/config.aws.js src/config.js + - name: Capture PR branch usage statistics + id: pr-stats + run: | + PR_STATS=$(node scripts/stats.js -j) + echo "PR_STATS<> $GITHUB_ENV + echo -e "$PR_STATS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + - name: Compare Stats + id: compare-stats + run: | + mkdir stats + echo '${{ env.MAIN_STATS }}' + echo '${{ env.PR_STATS }}' + npm exec ts-node scripts/stats_compare '${{ env.MAIN_STATS }}' '${{ env.PR_STATS }}' > stats/stats-difference.md + - name: Print Stats to GitHub Checks + uses: LouisBrunner/checks-action@v1.6.1 + if: always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Performance Metrics + conclusion: neutral + output: | + {"summary":"Style size changes introduced by this PR"} + output_text_description_file: stats/stats-difference.md - name: Upload Build artifact uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/pr-preview-check.yml b/.github/workflows/pr-preview-check.yml new file mode 100644 index 000000000..4577db136 --- /dev/null +++ b/.github/workflows/pr-preview-check.yml @@ -0,0 +1,53 @@ +name: Generate PR Preview Check +on: + pull_request: + branches: [main] + types: + - opened + - reopened + - synchronize + workflow_dispatch: +permissions: + checks: write +jobs: + deploy-preview: + runs-on: ubuntu-latest + steps: + - name: Wait for PR Preview Upload (1x Sprite) + uses: cygnetdigital/wait_for_response@v2.0.0 + with: + url: "https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/sprites/sprite.png" + responseCode: "200" + timeout: 120000 + interval: 500 + - name: Wait for PR Preview Upload (2x Sprite) + uses: cygnetdigital/wait_for_response@v2.0.0 + with: + url: "https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/sprites/sprite@2x.png" + responseCode: "200" + timeout: 120000 + interval: 500 + - name: Generate Preview text + run: | + echo "## PR Preview: + * [Map](https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/) + * [Shield Test](https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/shieldtest.html) + * [style.json](https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/style.json) + * [shields.json](https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/shields.json) + * [taginfo.json](https://preview.ourmap.us/pr/${{ github.event.pull_request.number }}/taginfo.json) + + ## Sprite Sheets: + + + + " > pr_preview.md + - name: Print Preview Links to GitHub Checks + uses: LouisBrunner/checks-action@v1.6.1 + if: always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: PR Preview + conclusion: neutral + output: | + {"summary":"Preview map changes introduced by this PR"} + output_text_description_file: pr_preview.md diff --git a/.github/workflows/s3-upload.yml b/.github/workflows/s3-upload.yml index 351cbc58e..81dc0df57 100644 --- a/.github/workflows/s3-upload.yml +++ b/.github/workflows/s3-upload.yml @@ -72,20 +72,3 @@ jobs: AWS_REGION: ${{ secrets.AWS_REGION }} SOURCE_DIR: "./dist" DEST_DIR: pr/${{ env.PR_NUM }} - - name: Comment Pull Request - uses: thollander/actions-comment-pull-request@v2.3.1 - with: - message: | - PR Preview: - * [Map](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/) - * [Shield Test](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/shieldtest.html) - * [style.json](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/style.json) - * [shields.json](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/shields.json) - * [taginfo.json](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/taginfo.json) - - Sprite Sheets: - - ![Sprites 1x](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/sprites/sprite.png) - ![Sprites 2x](https://preview.ourmap.us/pr/${{ env.PR_NUM }}/sprites/sprite@2x.png) - comment_tag: pr_preview - pr_number: ${{ env.PR_NUM }} diff --git a/scripts/object_compare.ts b/scripts/object_compare.ts new file mode 100644 index 000000000..d93461cb4 --- /dev/null +++ b/scripts/object_compare.ts @@ -0,0 +1,145 @@ +/** + * Calculates the difference between two objects by comparing their properties. + * If a property value is an object, it recursively calls itself to calculate the difference. + * Positive numbers for a property means that property is greater in object2 than object1. + * @param {Object|null} object1 - The first object to compare. + * @param {Object|null} object2 - The second object to compare. + * @returns {Object} - An object containing the differences between object2 and object1. + */ +export function calculateDifference( + object1: object | null, + object2: object | null +): object { + // If one object exists and the other doesn't, return the difference + if (object1 === null && object2 !== null) { + return object2; + } else if (object2 === null && object1 !== null) { + return negate(object1); + } + + const difference = {}; + + // Iterate through each property in object1 + for (const key in object1) { + if (typeof object1[key] === "object" && typeof object2![key] === "object") { + // Recursively calculate the difference for nested objects + difference[key] = calculateDifference(object1[key], object2![key]); + } else if ( + typeof object1[key] === "number" && + typeof object2![key] === "number" + ) { + // Calculate the difference for numeric properties + difference[key] = object2![key] - object1[key]; + } else { + // If the property exists in object1 but not in object2, include it in the result + difference[key] = negate(object1![key]); + } + } + + // Include properties that exist in object2 but not in object1 + for (const key in object2!) { + if (!(key in object1!)) { + difference[key] = object2[key]; + } + } + + return difference; +} + +/** + * Negate all numeric properties of this object. + * @param {Object} object - The object to process. + */ +function negate(object: object) { + if (typeof object === "number") { + return -object; + } + + // Create a new object to store the result + const result = {}; + + for (const key in object) { + if (typeof object[key] === "object" && object[key] !== null) { + // If the property value is an object (and not null), recursively process it + result[key] = negate(object[key]); + } else if (typeof object[key] === "number") { + // If the property value is a number, multiply it by the multiplier + result[key] = -object[key]; + } + } + + return result; +} + +// "| | main | this PR | change | % change |", +export type ComparedStats = { + name: string; + beforeValue: number | null; + afterValue: number | null; + change: number; + pctChange: number | null; +}; + +export function statsComparisonRow( + name: string, + val1: number | null, + val2: number | null, + change: number +): ComparedStats { + let pctChange: number | null; + + if (val1 !== null) { + if (val2 !== null) { + pctChange = change / val1; + } else { + pctChange = -1; + } + } else { + pctChange = null; + } + + return { + name, + beforeValue: val1, + afterValue: val2, + change, + pctChange, + }; +} + +const pctFormat: Intl.NumberFormatOptions = { + style: "percent", + minimumFractionDigits: 1, + maximumFractionDigits: 1, + signDisplay: "exceptZero", +}; + +function naLocString(val: number | null) { + return val !== null ? val.toLocaleString("en") : "N/A"; +} + +/** + * produce a markdown row of statistics comparison + */ +export function mdStringValues(stats: ComparedStats): string[] { + const beforeValueStr = naLocString(stats.beforeValue); + const afterValueStr = naLocString(stats.afterValue); + const changeStr = naLocString(stats.change); + const pctChangeStr = + stats.pctChange !== null + ? stats.pctChange.toLocaleString("en", pctFormat) + : "N/A"; + + return [stats.name, beforeValueStr, afterValueStr, changeStr, pctChangeStr]; +} + +export function mdCompareRow( + name: string, + val1: number | null, + val2: number | null, + change: number +): string { + return mdStringValues(statsComparisonRow(name, val1, val2, change)).join( + " | " + ); +} diff --git a/scripts/stats_compare.js b/scripts/stats_compare.js new file mode 100644 index 000000000..45f70dad7 --- /dev/null +++ b/scripts/stats_compare.js @@ -0,0 +1,81 @@ +import { calculateDifference, mdCompareRow } from "./object_compare"; + +const stats1 = JSON.parse(process.argv[2]); +const stats2 = JSON.parse(process.argv[3]); + +const difference = calculateDifference(stats1, stats2); + +const diffHeaderRow = [ + "| | main | this PR | change | % change |", + "|-----------|--------------:|-------------:|----------------:|----------------:|", +]; + +/** + * Show comparison of overall aggregate statistics between this PR and previous + */ + +const layersRow = mdCompareRow( + "Layers", + stats1.layerCount, + stats2.layerCount, + difference.layerCount +); + +const sizeRow = mdCompareRow( + "Size (b)", + stats1.styleSize, + stats2.styleSize, + difference.styleSize +); + +printTable("Style size statistics", [layersRow, sizeRow]); + +/** + * Show comparison of the number of layers in each group before and after + */ + +const layerCountChangeRows = []; + +for (const layer in difference.layerGroup) { + layerCountChangeRows.push( + mdCompareRow( + layer, + stats1.layerGroup[layer]?.layerCount, + stats2.layerGroup[layer]?.layerCount, + difference.layerGroup[layer]?.layerCount + ) + ); +} + +printTable("Layer count comparison", layerCountChangeRows); + +/** + * Show comparison of the aggregate size of layers in each group before and after + */ + +const layerSizeChangeRows = []; + +for (const layer in difference.layerGroup) { + layerSizeChangeRows.push( + mdCompareRow( + layer, + stats1.layerGroup[layer]?.size, + stats2.layerGroup[layer]?.size, + difference.layerGroup[layer]?.size + ) + ); +} + +printTable("Layer size comparison", layerSizeChangeRows); + +function printTable(headingText, rows) { + const table = [...diffHeaderRow, ...rows].join("\n"); + const text = ` + +## ${headingText} + +${table} +`; + + console.log(text); +} diff --git a/test/stats_compare/stats_compare.spec.ts b/test/stats_compare/stats_compare.spec.ts new file mode 100644 index 000000000..4ef400e66 --- /dev/null +++ b/test/stats_compare/stats_compare.spec.ts @@ -0,0 +1,61 @@ +import { + calculateDifference, + mdCompareRow, +} from "../../scripts/object_compare"; +import { expect } from "chai"; + +const a = 3; +const b = 4; + +const simpleA = { a }; +const simpleB = { b }; + +const complexA = { foo: { a } }; +const complexB = { bar: { b } }; + +const simpleA0 = { a: 0 }; +const complexA0 = { foo: { a: 0 } }; + +const negSimpleA = { a: -a }; +const negComplexA = { foo: { a: -a } }; + +const diffSimpleAB = { a: -a, b }; +const diffComplexAB = { foo: { a: -a }, bar: { b } }; + +const aStr = a.toLocaleString("en"); + +const simpleAAmdRow = `a | ${aStr} | ${aStr} | 0 | 0.0%`; +const simpleAnullmdRow = `a | ${aStr} | N/A | ${-aStr} | -100.0%`; + +describe("stats_compare", function () { + describe("#calculateDifference", function () { + it("tests stats equality", function () { + expect(calculateDifference(simpleA, simpleA)).to.deep.equal(simpleA0); + expect(calculateDifference(complexA, complexA)).to.deep.equal(complexA0); + }); + it("tests stats remove", function () { + expect(calculateDifference(simpleA, null)).to.deep.equal(negSimpleA); + expect(calculateDifference(complexA, null)).to.deep.equal(negComplexA); + }); + it("tests stats add", function () { + expect(calculateDifference(null, simpleA)).to.deep.equal(simpleA); + expect(calculateDifference(null, complexA)).to.deep.equal(complexA); + }); + it("tests stats diff", function () { + expect(calculateDifference(simpleA, simpleB)).to.deep.equal(diffSimpleAB); + expect(calculateDifference(complexA, complexB)).to.deep.equal( + diffComplexAB + ); + }); + }); + describe("#mdCompareRow", function () { + it("tests md compare same", function () { + expect(mdCompareRow("a", simpleA.a, simpleA.a, simpleA0.a)).to.deep.equal( + simpleAAmdRow + ); + expect(mdCompareRow("a", simpleA.a, null, negSimpleA.a)).to.deep.equal( + simpleAnullmdRow + ); + }); + }); +});