QA How-To
Run Playwright Tests on GitHub Actions ARM64 (2026)
Learn to run Playwright tests GitHub Actions ARM64 with native runners, Chromium setup, architecture checks, caching, artifacts, and practical CI fixes.
24 min read | 2,871 words
TL;DR
Use an ARM64 GitHub Actions runner, assert that `uname -m` returns `aarch64`, install locked npm packages, run `npx playwright install --with-deps chromium`, and execute `npx playwright test`. Upload the report and test-results directories with `if: ${{ !cancelled() }}` so ARM-specific failures remain diagnosable.
Key Takeaways
- Select an actual ARM64 runner and prove its architecture before installing dependencies.
- Keep the Playwright package and browser revision locked through package-lock.json.
- Install only Chromium with its Linux dependencies when Chromium is the required coverage.
- Use Playwright webServer readiness instead of fixed sleeps when testing a local application.
- Upload HTML reports, traces, screenshots, and JUnit XML even when tests fail.
- Treat native ARM64 and emulated x64 as different execution models with different failure modes.
- Label and harden self-hosted ARM64 runners before allowing repository code to execute on them.
To run playwright tests github actions arm64 successfully, choose a native ARM64 runner, verify the machine architecture before setup, install the Playwright-managed ARM64 browser and Linux dependencies, then run the same locked test command you use locally. The critical detail is that every layer must agree on architecture: runner, Node.js, native npm packages, browser binary, and any container image.
This tutorial builds a small TypeScript suite and a production-ready workflow for Ubuntu 24.04 ARM64. You will also add architecture guards, deterministic installs, application readiness, reports, traces, and a focused troubleshooting routine. For a broader CI overview, read GitHub Actions for Playwright, then use this guide for the ARM-specific implementation.
What You Will Build
You will create a repository setup that:
- Runs a real Playwright Chromium test on a native GitHub-hosted ARM64 runner.
- Fails immediately if the runner or Node process is not ARM64.
- Builds and starts a tiny local web application through Playwright's
webServersupport. - Produces an HTML report, JUnit XML, trace, screenshot, and failure-only video.
- Uploads diagnostic artifacts after both passing and failing test runs.
- Provides a safe adaptation path for labeled self-hosted ARM64 machines.
The final pipeline uses normal npm and Playwright commands. You can reproduce it on an ARM64 Linux workstation without copying GitHub-only logic into the test suite.
Prerequisites
Use these exact tutorial versions:
| Component | Tutorial version | Why it matters |
|---|---|---|
| Ubuntu runner image | Ubuntu 24.04 ARM64 | Matches the workflow label and supported Linux baseline |
| Node.js | 22.x | Current LTS line for this tutorial |
| npm | 10 or newer | Supports the committed lockfile workflow |
@playwright/test |
1.55.0 | Pins the test runner and matching browser revision |
| TypeScript | 5.9.2 | Type-checks configuration and tests |
| GitHub checkout action | v5 | Checks out the exact workflow commit |
| GitHub setup-node action | v4 | Installs ARM64 Node 22 and caches npm downloads |
| GitHub upload-artifact action | v4 | Stores reports and failure evidence |
You need a GitHub repository with Actions enabled and permission to add .github/workflows/playwright-arm64.yml. The ubuntu-24.04-arm label is appropriate where GitHub-hosted ARM64 runners are available. If your repository or organization does not offer that label, use an approved ARM64 larger-runner label or the self-hosted adaptation in Step 8. Runner availability and billing depend on repository and organization settings, so confirm the label in the Actions runner selector before making the check required.
Install Git and Node 22 locally. On an ARM64 machine, verify both the kernel and Node architecture:
uname -m
node -p "process.arch"
node --version
npm --version
Expected output includes aarch64 from Linux, arm64 from Node, and a Node version beginning with v22.. An x64 development machine can still author the files, but it does not validate ARM-native browser launch behavior.
Step 1: Create the Locked Playwright Project
Start in an empty repository or a dedicated example directory. Initialize npm, install exact development dependencies, and create the folders used later:
npm init -y
npm install --save-dev --save-exact @playwright/test@1.55.0 typescript@5.9.2
mkdir -p tests public .github/workflows
The --save-exact flag records an exact package version. Commit both package.json and package-lock.json; GitHub Actions will use npm ci, which refuses an inconsistent lockfile instead of silently resolving a different graph. Playwright's npm package determines the expected browser revision, so package and browser installation must stay together.
Add these scripts to the generated package.json while retaining its other fields:
{
"scripts": {
"build": "node scripts/build.mjs",
"test:e2e": "playwright test",
"test:e2e:list": "playwright test --list",
"report:e2e": "playwright show-report"
}
}
You will create scripts/build.mjs in Step 3. The script name makes the CI command obvious and keeps the Playwright invocation reproducible outside GitHub. Do not install @playwright/test@latest in workflow YAML because that would bypass code review and the lockfile.
Verify Step 1: run the locked clean install and inspect the resolved version.
npm ci
npx playwright --version
npm ls @playwright/test typescript
The Playwright command should print Version 1.55.0, and npm ls should show the exact two requested versions without invalid or extraneous markers.
Step 2: Configure Chromium and ARM-Friendly CI Defaults
Create playwright.config.ts with one Chromium project. Limiting installation and execution to the browser you actually claim prevents unnecessary downloads and avoids confusing a missing Firefox binary with an ARM runner defect.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI
? [
["line"],
["html", { open: "never", outputFolder: "playwright-report" }],
["junit", { outputFile: "test-results/junit.xml" }]
]
: [["list"], ["html", { open: "never" }]],
use: {
baseURL: "http://127.0.0.1:4173",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure"
},
projects: [
{
name: "chromium-arm64",
use: { ...devices["Desktop Chrome"] }
}
],
webServer: {
command: "node dist/server.mjs",
url: "http://127.0.0.1:4173/health",
reuseExistingServer: !process.env.CI,
timeout: 30_000
}
});
One CI worker gives a small runner a predictable starting point. It is not a universal performance rule. After collecting timings, increase workers or shard independent tests. The first retry captures a trace while preserving the original failure signal. The HTML and JUnit reporters serve different consumers: humans explore the HTML bundle, while automation parses XML.
The health URL makes Playwright wait for a real response rather than guessing with sleep. If the server exits or misses the 30-second deadline, setup fails clearly before a page assertion runs.
Verify Step 2: ask Playwright to parse the configuration and list tests. There are no tests yet, so an empty list is expected, but configuration syntax must load successfully.
npx playwright test --list
Expected output ends with Total: 0 tests in 0 files. A TypeScript syntax or unknown configuration property error means the step is not complete.
Step 3: Build a Deterministic Test Application
Create scripts/build.mjs. This small build script writes both the page and a dependency-free Node server into dist. It avoids pulling an unrelated web framework into an ARM troubleshooting exercise.
import { mkdir, writeFile } from "node:fs/promises";
await mkdir("dist", { recursive: true });
await writeFile(
"dist/index.html",
`<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>ARM64 CI Check</title></head>
<body>
<main>
<h1>Playwright on ARM64</h1>
<button id="status">Check status</button>
<p role="status" aria-live="polite">Ready</p>
</main>
<script>
document.querySelector("#status").addEventListener("click", () => {
document.querySelector('[role="status"]').textContent = "ARM64 workflow passed";
});
</script>
</body>
</html>`
);
await writeFile(
"dist/server.mjs",
`import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
const page = await readFile(new URL("./index.html", import.meta.url));
createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200, { "content-type": "text/plain" });
response.end("ok");
return;
}
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
response.end(page);
}).listen(4173, "127.0.0.1");`
);
The server binds to the same host and port declared in Playwright configuration. The /health path returns a direct readiness response, while all other paths serve the page. In a real project, retain your existing build command and point webServer at the actual preview service.
Verify Step 3: build, start the generated server, request its health endpoint, then stop it with Ctrl+C. Use two terminals for the last two commands.
npm run build
node dist/server.mjs
curl --fail http://127.0.0.1:4173/health
The request must print ok. If it returns connection refused, compare the configured host, port, and generated file path before touching Playwright timeouts.
Step 4: Write a Runnable Browser Test
Create tests/arm64.spec.ts. The test uses role-based locators, checks the initial UI, performs one action, and verifies an accessible status update.
import { expect, test } from "@playwright/test";
test("updates status in Chromium on ARM64 CI", async ({ page }) => {
await page.goto("/");
await expect(
page.getByRole("heading", { name: "Playwright on ARM64" })
).toBeVisible();
await page.getByRole("button", { name: "Check status" }).click();
await expect(page.getByRole("status")).toHaveText(
"ARM64 workflow passed"
);
});
This is a browser test, not merely a process smoke check. Chromium must launch, navigate to the server, create an accessibility tree for the role locators, execute JavaScript, and expose the changed text. The test itself is architecture-neutral, which is desirable. Architecture enforcement belongs in the workflow guard added next.
Install the matching local Chromium revision before the first local run:
npx playwright install chromium
npm run build
npm run test:e2e
On Ubuntu, add --with-deps when the machine lacks browser system packages. macOS developers normally install the browser without Linux dependency flags. For locator design beyond this sample, use the Playwright locator strategies guide.
Verify Step 4: the command should report one passed test in the chromium-arm64 project and create playwright-report/index.html. Open the report with npm run report:e2e if you want to inspect the recorded steps.
Step 5: Run Playwright Tests GitHub Actions ARM64 Natively
Create .github/workflows/playwright-arm64.yml. This is the minimum complete native ARM64 workflow, including an explicit architecture assertion.
name: Playwright ARM64
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: playwright-arm64-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Chromium on Ubuntu ARM64
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
steps:
- name: Check out source
uses: actions/checkout@v5
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: npm
- name: Prove native ARM64 execution
shell: bash
run: |
set -euo pipefail
test "$(uname -m)" = "aarch64"
test "$(node -p 'process.arch')" = "arm64"
uname -a
node --version
- name: Install locked dependencies
run: npm ci
- name: Install ARM64 Chromium and Linux dependencies
run: npx playwright install --with-deps chromium
- name: Build application
run: npm run build
- name: Run browser tests
run: npm run test:e2e
env:
CI: "true"
runs-on controls the runner architecture. The job does not become ARM64 because a variable says so. The two test commands catch a mislabeled self-hosted runner, an unexpected hosted label, or an x64 Node installation before expensive browser setup begins. Linux names the architecture aarch64; Node exposes the same architecture as arm64.
actions/setup-node resolves a Node 22 distribution for the runner architecture. npm ci can then select ARM64 variants for architecture-specific optional packages. The Playwright install command downloads the Chromium revision associated with the locked Playwright package and installs required Ubuntu libraries.
Verify Step 5: commit and push the files, open the workflow run, and expand Prove native ARM64 execution. It must show aarch64 and Node v22.x. The browser step must end with one passed test. If GitHub reports that no runner matches ubuntu-24.04-arm, do not delete the guard. Select an ARM64 label available to your repository or continue to Step 8.
Step 6: Preserve ARM64 Failure Evidence
A green status proves the example passed, but a useful CI system also explains red runs. Append this artifact step after Run browser tests at the same indentation level:
- name: Upload Playwright diagnostics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-arm64-${{ github.run_id }}-${{ github.run_attempt }}
path: |
playwright-report/
test-results/
if-no-files-found: warn
retention-days: 14
The condition runs after a test failure but skips needless work after cancellation. A unique name prevents a rerun from colliding with an earlier artifact. The HTML report includes the execution overview; test-results can contain JUnit XML, traces, screenshots, and retained video according to the configuration. The Playwright trace viewer tutorial explains how to inspect trace actions, DOM snapshots, console output, and network activity.
Avoid uploading the entire workspace. Test artifacts can contain URLs, rendered account details, response bodies, and screenshots, so use synthetic accounts and an appropriate retention period. Artifact access follows repository permissions and should be treated as sensitive.
Verify Step 6: temporarily change the expected text in the test to wrong value, push the branch, and let the check fail. The run summary must still contain the named artifact. Download it and confirm playwright-report/index.html exists. Restore the correct assertion before merging. This deliberate failure also proves the upload condition rather than merely proving the happy path.
Step 7: Add Caching Without Mixing Architectures
The workflow already sets cache: npm. That caches npm's download cache, not node_modules, and npm ci still reconstructs dependencies from the lockfile. This is a safe default because packages with native components are selected for the active ARM64 Node process.
For a monorepo or non-root lockfile, identify the dependency path explicitly:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: npm
cache-dependency-path: apps/web/package-lock.json
If you create a custom cache, include operating system, architecture, and a lockfile hash in the key. This example caches a hypothetical build tool directory, not Playwright browsers:
- name: Cache ARM64 build data
uses: actions/cache@v4
with:
path: .cache/build
key: build-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}
Do not share an x64 node_modules archive with ARM64. Native .node binaries can install successfully into the cache on one architecture and fail with Exec format error on another. Do not cache Playwright browser directories by reflex either. Browser revisions change with Playwright, Linux packages still need installation, and cache transfer may not beat a fresh download. Measure each workflow phase first. The GitHub Actions caching guide covers cache scope and invalidation in more detail.
Verify Step 7: run the workflow twice without changing package-lock.json. The setup-node log should report an npm cache hit on the second run, while npm ci and the architecture guard still execute. A cache hit must never replace dependency verification.
Step 8: Adapt the Workflow to a Self-Hosted ARM64 Runner
Use self-hosting when hosted ARM64 capacity, network access, hardware characteristics, or organization policy requires it. Register the runner with precise labels such as self-hosted, linux, arm64, and playwright. Then change only the job selector:
jobs:
test:
runs-on: [self-hosted, linux, arm64, playwright]
Keep the uname and Node assertions from Step 5. Labels are routing metadata, not proof. Install Node through actions/setup-node so the project does not accidentally inherit an administrator's old global runtime. Ensure the runner account can install Playwright's Linux dependencies, or pre-provision the documented packages in a versioned machine image.
A persistent runner has a larger threat surface than a disposable hosted runner. Do not route untrusted fork pull requests to a machine with internal network access, cloud credentials, Docker control, or reusable workspaces. Prefer ephemeral runners that accept one job and are destroyed. If persistence is unavoidable, isolate the account, clean known work directories, restrict outbound and internal access, patch the host, and monitor runner registration tokens.
Containers add another constraint: the image itself must publish a Linux ARM64 manifest. An x64-only Playwright image either fails to start or runs through emulation, which changes performance and browser behavior. The Docker for Playwright guide is useful once native host execution is stable.
Verify Step 8: use workflow_dispatch, inspect the job header for your expected runner name and labels, then read the architecture guard output. Confirm the job is assigned only to the hardened pool and that uname -m still returns aarch64.
Step 9: Scale Run Playwright Tests GitHub Actions ARM64 Pipelines Carefully
First measure queue time, checkout, Node setup, npm install, browser installation, application build, test time, and artifact upload. ARM64 is not automatically faster or slower for every suite. Browser workload, runner size, native dependencies, server capacity, and queue availability all influence feedback time. Compare equivalent runner resources and identical commits.
For a large isolated suite, Playwright can shard tests across several ARM64 jobs:
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
# Repeat checkout, Node, architecture guard, npm ci, browser install, and build.
- name: Run shard
run: npx playwright test --shard=${{ matrix.shard }}/4
env:
CI: "true"
This fragment is intentionally added to the complete job from Step 5, not used alone. Set fail-fast: false so one failure does not erase evidence from the other shards. Give each shard separate accounts and mutable records. Shared test data often creates more instability than extra compute removes. For complete report merging, follow Playwright sharding across machines.
Keep pull request coverage representative. A Chromium ARM64 gate can detect architecture-specific installation and runtime problems, while scheduled or release workflows cover other browsers, operating systems, and x64. A matrix containing every browser, architecture, locale, role, and feature flag can consume substantial capacity without proportionate risk coverage.
Verify Step 9: check that the Actions run shows four ARM64 matrix jobs, each logs a different --shard=N/4 value, and the combined discovered test count equals the unsharded count. With this one-test example, sharding is educational but wasteful; enable it only after the suite is large enough to benefit.
Troubleshooting
Problem: The job remains queued with no matching runner -> Confirm that ubuntu-24.04-arm is available for the repository. Organization-owned larger runners use configured labels, while a self-hosted runner must be online and carry every label in runs-on. Do not fall back to ubuntu-latest if native ARM64 coverage is the requirement because that commonly selects x64.
Problem: The architecture guard prints x64 or fails -> Inspect both uname -m and node -p 'process.arch'. A mismatched Node binary, container platform, or runner label can produce mixed architecture. Reinstall Node through setup-node on the native host and remove any x64-only container override.
Problem: Chromium reports Exec format error -> Delete any restored browser or node_modules cache that came from x64, rerun npm ci, and execute npx playwright install --with-deps chromium on ARM64. Check custom cache keys for ${{ runner.arch }}. This error indicates the kernel received a binary for the wrong architecture, not a locator timeout.
Problem: Browser launch reports missing shared libraries -> Run the install command with --with-deps on a supported Ubuntu base. On a locked self-hosted image, pre-provision the required OS packages and update the image alongside Playwright. Do not solve a native library error by increasing the test timeout. The browser executable troubleshooting guide helps distinguish a missing download from missing Linux libraries.
Problem: The health URL times out although the build passed -> Run node dist/server.mjs and curl --fail http://127.0.0.1:4173/health in the runner environment. Verify that host, port, path, and build output match webServer. Review the server process exit output. A fixed sleep hides these mismatches and makes slow starts less predictable.
Problem: Tests pass on x64 but fail intermittently on ARM64 -> Compare exact Node, Playwright, browser revision, runner image, CPU allocation, locale, timezone, and application build. Open the trace from the first retry and find the earliest divergence. Avoid multiplying global timeouts; fix test-data races, readiness gaps, or native dependency differences. The Playwright timeout fix guide gives a structured synchronization checklist.
Interview Questions and Answers
The JSON interview section below contains model answers you can practice. The strongest interview response explains the architecture chain, proves assumptions with commands, and separates runner provisioning from Playwright test design. Be ready to discuss why native execution matters, how caches cross architecture boundaries, and how you would secure a self-hosted runner.
Common Mistakes
- Choosing
ubuntu-latestand assuming it means ARM64. - Removing the architecture check after the first green run.
- Installing the newest Playwright package during CI instead of using the lockfile.
- Restoring x64
node_modulesor browser caches on an ARM64 job. - Installing all browsers when the workflow only promises Chromium coverage.
- Using a fixed sleep instead of probing application readiness.
- Uploading reports only after success, leaving failures without traces.
- Treating a passing retry as evidence that the first failure did not matter.
- Sending fork pull requests to a privileged self-hosted runner.
- Using an x64-only container through emulation while claiming native ARM64 results.
- Adding shards before isolating test accounts and mutable data.
- Increasing timeouts before checking CPU, server startup, and binary architecture.
Where To Go Next
Make the single Chromium ARM64 job stable and required before expanding the matrix. Then use GitHub Actions matrix testing to add only combinations tied to product risk, and adopt parallel test sharding in CI when measured test duration justifies extra runners.
For environment authentication without long-lived cloud keys, continue with GitHub Actions OIDC for test environments. If your suite needs a stronger project structure before scaling CI, build it with the Playwright TypeScript framework tutorial.
The finished workflow now proves native ARM64 at both the operating-system and Node layers, installs the locked Playwright browser revision, waits for a real application health endpoint, and preserves actionable evidence. That is the repeatable foundation needed to run playwright tests github actions arm64 without confusing runner selection, binary compatibility, and test reliability.
Interview Questions and Answers
How would you design a Playwright pipeline for GitHub Actions ARM64?
I would select a native ARM64 runner, assert `aarch64` at the kernel and `arm64` in Node, install dependencies through `npm ci`, and install only the configured Playwright browsers with Linux dependencies. The job would start or target the application through an explicit readiness check, run the normal project command, and upload reports and traces after failures.
Why should a workflow check both uname and process.arch?
They validate different layers. `uname -m` identifies the Linux kernel architecture, while `process.arch` identifies the Node binary executing package scripts. A mixed installation or emulated container can make those values disagree, so checking both prevents a false native-ARM claim.
How do you avoid cross-architecture cache failures in CI?
I avoid caching `node_modules` and let `npm ci` rebuild it from the lockfile. Custom cache keys include runner OS, runner architecture, tool version inputs, and a lockfile hash. I also measure browser downloads before caching them because Playwright revisions and Linux dependencies complicate reuse.
What does Playwright install --with-deps chromium do on an ARM64 runner?
It installs the Chromium revision expected by the locked Playwright package and ensures required Linux browser dependencies are present. Restricting the command to Chromium aligns installation with the configured project. It should execute on the target ARM64 runner so the installed binary matches that environment.
How would you diagnose a test that passes on x64 but fails on ARM64?
I first separate binary launch failure, environment readiness, product behavior, and assertion failure. I compare runner image, Node version, Playwright version, browser revision, native packages, CPU allocation, locale, timezone, and the tested build. Then I inspect the first failure trace and reproduce with the locked commands on a native ARM64 host.
What security concerns apply to a self-hosted ARM64 Playwright runner?
Test code can read the workspace, reach permitted networks, launch processes, and potentially access host credentials. I keep untrusted fork code off privileged runners, minimize token permissions, isolate network access, patch the host, and prefer one-job ephemeral instances. Persistent machines require disciplined cleanup and monitoring but cleanup alone is not a complete trust boundary.
When would you shard Playwright tests across ARM64 runners?
I shard after measuring a suite whose execution time dominates setup and after removing shared-data coupling. Each shard receives the same commit, configuration, package lock, browser revision, and architecture guard. I disable matrix fail-fast, isolate test data, and merge evidence so parallelism does not reduce diagnosability.
Frequently Asked Questions
Can GitHub Actions run Playwright tests on ARM64?
Yes. Select a GitHub-hosted or self-hosted Linux ARM64 runner, install the locked npm dependencies, then run `npx playwright install --with-deps chromium` and `npx playwright test`. Add architecture assertions because a label alone does not prove that every runtime layer is native ARM64.
Which GitHub Actions runner label should I use for Playwright ARM64 tests?
Use `ubuntu-24.04-arm` where that GitHub-hosted label is available. Organizations using ARM64 larger runners or self-hosted machines must use the labels configured for their runner pool, then verify `uname -m` returns `aarch64`.
How do I verify that a Playwright job is really running on ARM64?
Run `uname -m` and require `aarch64`, then evaluate `node -p 'process.arch'` and require `arm64`. Keeping both assertions catches an ARM64 host with an incorrectly installed x64 Node runtime or an unintended emulated container.
Why does Playwright Chromium fail with Exec format error on ARM64?
The runner is attempting to execute a binary built for another architecture. Remove x64 browser and dependency caches, run `npm ci` on the ARM64 host, and reinstall Chromium with the Playwright version locked in the repository.
Should I cache Playwright browsers in an ARM64 GitHub Actions workflow?
Start without a browser cache. The browser revision follows Playwright, Linux dependencies still need installation, and cache restoration may not improve total time. If measurement supports caching, include operating system, runner architecture, and the lockfile hash in the key.
Can I use a self-hosted Raspberry Pi or cloud ARM VM for Playwright Actions?
A supported 64-bit Linux ARM host can act as a self-hosted runner when it satisfies Playwright's operating-system and browser requirements. Apply precise labels, keep the architecture guard, isolate untrusted code, and prefer an ephemeral machine because browser tests execute repository-controlled code.
Does an ARM64 runner make Playwright tests faster?
Not inherently. Compare equivalent CPU and memory resources while measuring queue, install, browser startup, application, test, and artifact phases. Native dependency support and runner availability can matter more than instruction-set architecture alone.