Skip to content

Commit

Permalink
Merge pull request #5063 from Shopify/lopert.wasm-opt
Browse files Browse the repository at this point in the history
Download and run wasm-opt
  • Loading branch information
lopert authored Dec 13, 2024
2 parents 011fc50 + 219b31c commit 17f19d1
Show file tree
Hide file tree
Showing 15 changed files with 185 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ function defaultFunctionConfiguration(): FunctionConfigType {
build: {
command: 'echo "hello world"',
watch: ['src/**/*.rs'],
wasm_opt: true,
},
api_version: '2022-07',
configuration_ui: true,
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/models/app/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ describe('validateFunctionExtensionsWithUiHandle', () => {
description: 'description',
build: {
command: 'echo "hello world"',
wasm_opt: true,
},
api_version: '2022-07',
configuration_ui: true,
Expand Down
14 changes: 11 additions & 3 deletions packages/app/src/cli/models/extensions/extension-instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ function functionConfiguration(): FunctionConfigType {
api_version: '2023-07',
configuration_ui: true,
metafields: [],
build: {},
build: {
wasm_opt: true,
},
}
}

Expand All @@ -41,6 +43,7 @@ describe('watchPaths', async () => {
const config = functionConfiguration()
config.build = {
watch: 'src/single-path.foo',
wasm_opt: true,
}
const extensionInstance = await testFunctionExtension({
config,
Expand All @@ -54,7 +57,9 @@ describe('watchPaths', async () => {

test('returns default paths for javascript', async () => {
const config = functionConfiguration()
config.build = {}
config.build = {
wasm_opt: true,
}
const extensionInstance = await testFunctionExtension({
config,
entryPath: 'src/index.js',
Expand Down Expand Up @@ -86,6 +91,7 @@ describe('watchPaths', async () => {
const config = functionConfiguration()
config.build = {
watch: ['src/**/*.rs', 'src/**/*.foo'],
wasm_opt: true,
}
const extensionInstance = await testFunctionExtension({
config,
Expand All @@ -103,7 +109,9 @@ describe('watchPaths', async () => {

test('returns null if not javascript and not configured', async () => {
const config = functionConfiguration()
config.build = {}
config.build = {
wasm_opt: true,
}
const extensionInstance = await testFunctionExtension({
config,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('functionConfiguration', () => {
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
},
ui: {
paths: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const FunctionExtensionSchema = BaseSchema.extend({
.optional(),
path: zod.string().optional(),
watch: zod.union([zod.string(), zod.string().array()]).optional(),
wasm_opt: zod.boolean().optional().default(true),
}),
configuration_ui: zod.boolean().optional().default(true),
ui: zod
Expand Down
44 changes: 43 additions & 1 deletion packages/app/src/cli/services/build/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {buildFunctionExtension} from './extension.js'
import {testFunctionExtension} from '../../models/app/app.test-data.js'
import {buildJSFunction} from '../function/build.js'
import {buildJSFunction, runWasmOpt} from '../function/build.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {exec} from '@shopify/cli-kit/node/system'
import lockfile from 'proper-lockfile'
import {AbortError} from '@shopify/cli-kit/node/error'
import {fileExistsSync} from '@shopify/cli-kit/node/fs'

vi.mock('@shopify/cli-kit/node/system')
vi.mock('../function/build.js')
vi.mock('proper-lockfile')
vi.mock('@shopify/cli-kit/node/fs')

describe('buildFunctionExtension', () => {
let extension: ExtensionInstance<FunctionConfigType>
Expand All @@ -26,6 +28,7 @@ describe('buildFunctionExtension', () => {
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
},
configuration_ui: true,
api_version: '2022-07',
Expand Down Expand Up @@ -138,6 +141,45 @@ describe('buildFunctionExtension', () => {
expect(releaseLock).toHaveBeenCalled()
})

test('performs wasm-opt execution by default', async () => {
// Given
vi.mocked(fileExistsSync).mockResolvedValue(true)

// When
await expect(
buildFunctionExtension(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
}),
).resolves.toBeUndefined()

// Then
expect(runWasmOpt).toHaveBeenCalled()
})

test('skips wasm-opt execution when the disable-wasm-opt is true', async () => {
// Given
vi.mocked(fileExistsSync).mockResolvedValue(true)
extension.configuration.build.wasm_opt = false

// When
await expect(
buildFunctionExtension(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
}),
).resolves.toBeUndefined()

// Then
expect(runWasmOpt).not.toHaveBeenCalled()
})

test('fails when build lock cannot be acquired', async () => {
// Given
vi.mocked(lockfile.lock).mockRejectedValue('failed to acquire lock')
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/cli/services/build/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {runThemeCheck} from './theme-check.js'
import {AppInterface} from '../../models/app/app.js'
import {bundleExtension, bundleFlowTemplateExtension} from '../extensions/bundle.js'
import {buildJSFunction} from '../function/build.js'
import {buildJSFunction, runWasmOpt} from '../function/build.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
import {exec} from '@shopify/cli-kit/node/system'
Expand Down Expand Up @@ -175,6 +175,12 @@ export async function buildFunctionExtension(
} else {
await buildOtherFunction(extension, options)
}

const wasmOpt = (extension as ExtensionInstance<FunctionConfigType>).configuration.build.wasm_opt
if (fileExistsSync(extension.outputPath) && wasmOpt) {
await runWasmOpt(extension.outputPath)
}

if (fileExistsSync(extension.outputPath) && bundlePath !== extension.outputPath) {
const base64Contents = await readFile(extension.outputPath, {encoding: 'base64'})
await touchFile(bundlePath)
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/context/id-matching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ beforeAll(async () => {
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
},
configuration_ui: false,
api_version: '2022-07',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ beforeAll(async () => {
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
},
metafields: [],
configuration_ui: false,
Expand Down
34 changes: 33 additions & 1 deletion packages/app/src/cli/services/function/binaries.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {javyBinary, functionRunnerBinary, downloadBinary, javyPluginBinary} from './binaries.js'
import {javyBinary, functionRunnerBinary, downloadBinary, javyPluginBinary, wasmOptBinary} from './binaries.js'
import {fetch, Response} from '@shopify/cli-kit/node/http'
import {fileExists, removeFile} from '@shopify/cli-kit/node/fs'
import {describe, expect, test, vi} from 'vitest'
Expand Down Expand Up @@ -279,3 +279,35 @@ describe('functionRunner', () => {
await expect(fileExists(functionRunner.path)).resolves.toBeTruthy()
})
})

describe('wasm-opt', () => {
const wasmOpt = wasmOptBinary()

test('properties are set correctly', () => {
expect(wasmOpt.name).toBe('wasm-opt.cjs')
expect(wasmOpt.version).match(/\d.\d.\d$/)
expect(wasmOpt.path).toMatch(/(\/|\\)wasm-opt.cjs$/)
})

test('downloadUrl returns the correct URL', () => {
// When
const url = wasmOpt.downloadUrl('', '')

// Then
expect(url).toMatch(/https:\/\/cdn.jsdelivr.net\/npm\/binaryen@\d{3}\.\d\.\d\/bin\/wasm-opt/)
})

test('downloads wasm-opt', async () => {
// Given
await removeFile(wasmOpt.path)
await expect(fileExists(wasmOpt.path)).resolves.toBeFalsy()
vi.mocked(fetch).mockResolvedValue(new Response('wasm-opt'))

// When
await downloadBinary(wasmOpt)

// Then
expect(fetch).toHaveBeenCalledOnce()
await expect(fileExists(wasmOpt.path)).resolves.toBeTruthy()
})
})
31 changes: 31 additions & 0 deletions packages/app/src/cli/services/function/binaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const JAVY_VERSION = 'v4.0.0'
// match the plugin version used in the function-runner version specified above.
const JAVY_PLUGIN_VERSION = 'v3.2.0'

const BINARYEN_VERSION = '120.0.0'

interface DownloadableBinary {
path: string
name: string
Expand Down Expand Up @@ -107,9 +109,30 @@ class JavyPlugin implements DownloadableBinary {
}
}

class WasmOptExecutable implements DownloadableBinary {
readonly name: string
readonly version: string
readonly path: string

constructor(name: string, version: string) {
this.name = name
this.version = version
this.path = joinPath(dirname(fileURLToPath(import.meta.url)), '..', 'bin', name)
}

downloadUrl(_processPlatform: string, _processArch: string) {
return `https://cdn.jsdelivr.net/npm/binaryen@${this.version}/bin/wasm-opt`
}

async processResponse(responseStream: PipelineSource<unknown>, outputStream: fs.WriteStream): Promise<void> {
await stream.pipeline(responseStream, outputStream)
}
}

let _javy: DownloadableBinary
let _javyPlugin: DownloadableBinary
let _functionRunner: DownloadableBinary
let _wasmOpt: DownloadableBinary

export function javyBinary() {
if (!_javy) {
Expand All @@ -132,6 +155,14 @@ export function functionRunnerBinary() {
return _functionRunner
}

export function wasmOptBinary() {
if (!_wasmOpt) {
_wasmOpt = new WasmOptExecutable('wasm-opt.cjs', BINARYEN_VERSION)
}

return _wasmOpt
}

export async function downloadBinary(bin: DownloadableBinary) {
const isDownloaded = await fileExists(bin.path)
if (isDownloaded) {
Expand Down
27 changes: 24 additions & 3 deletions packages/app/src/cli/services/function/build.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {buildGraphqlTypes, bundleExtension, runJavy, ExportJavyBuilder, jsExports} from './build.js'
import {javyBinary, javyPluginBinary} from './binaries.js'
import {buildGraphqlTypes, bundleExtension, runJavy, ExportJavyBuilder, jsExports, runWasmOpt} from './build.js'
import {javyBinary, javyPluginBinary, wasmOptBinary} from './binaries.js'
import {testApp, testFunctionExtension} from '../../models/app/app.test-data.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'
import {exec} from '@shopify/cli-kit/node/system'
import {joinPath} from '@shopify/cli-kit/node/path'
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
import {inTemporaryDirectory, mkdir, writeFile, removeFile} from '@shopify/cli-kit/node/fs'
import {build as esBuild} from 'esbuild'

Expand Down Expand Up @@ -183,6 +183,27 @@ describe('runJavy', () => {
})
})

describe('runWasmOpt', () => {
test('runs wasm-opt on the module', async () => {
// Given
const ourFunction = await testFunctionExtension()
const modulePath = ourFunction.outputPath

// When
const got = runWasmOpt(modulePath)

// Then
await expect(got).resolves.toBeUndefined()
expect(exec).toHaveBeenCalledWith(
'node',
[wasmOptBinary().name, modulePath, '-Oz', '--enable-bulk-memory', '--strip-debug', '-o', modulePath],
{
cwd: dirname(wasmOptBinary().path),
},
)
})
})

describe('ExportJavyBuilder', () => {
const exports = ['foo-bar', 'foo-baz']
const builder = new ExportJavyBuilder(exports)
Expand Down
29 changes: 27 additions & 2 deletions packages/app/src/cli/services/function/build.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {downloadBinary, javyBinary, javyPluginBinary} from './binaries.js'
import {downloadBinary, javyBinary, javyPluginBinary, wasmOptBinary} from './binaries.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
import {AppInterface} from '../../models/app/app.js'
import {EsbuildEnvVarRegex} from '../../constants.js'
import {hyphenate, camelize} from '@shopify/cli-kit/common/string'
import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/output'
import {exec} from '@shopify/cli-kit/node/system'
import {joinPath} from '@shopify/cli-kit/node/path'
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
import {build as esBuild, BuildResult} from 'esbuild'
import {findPathUp, inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs'
import {AbortSignal} from '@shopify/cli-kit/node/abort'
Expand Down Expand Up @@ -186,6 +186,31 @@ function getESBuildOptions(
return esbuildOptions
}

export async function runWasmOpt(modulePath: string) {
const wasmOpt = wasmOptBinary()
await downloadBinary(wasmOpt)

const wasmOptDir = dirname(wasmOptBinary().path)

const command = `node`
const args = [
// invoke the js-wrapped wasm-opt binary
wasmOptBinary().name,
modulePath,
// pass these options to wasm-opt
'-Oz',
'--enable-bulk-memory',
'--strip-debug',
// overwrite our existing module with the optimized version
'-o',
modulePath,
]

outputDebug(`Wasm binary: ${wasmOptBinary().name}`)
outputDebug('Optimizing this wasm binary using wasm-opt.')
await exec(command, args, {cwd: wasmOptDir})
}

export async function runJavy(
fun: ExtensionInstance<FunctionConfigType>,
options: JSFunctionBuildOptions,
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/function/replay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('replay', () => {
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
},
configuration_ui: true,
api_version: '2022-07',
Expand Down
Loading

0 comments on commit 17f19d1

Please sign in to comment.