Initial API

This commit is contained in:
jay-tux 2025-04-21 11:27:13 +02:00
commit 5f91256c31
Signed by: jay-tux
GPG Key ID: 84302006B056926E
23 changed files with 1317 additions and 0 deletions

45
api/.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/*
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Kotlin ###
.kotlin
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
.env
intrinsics.sqlite

133
api/README.md Normal file
View File

@ -0,0 +1,133 @@
# Intrinsics API
*The backend API powering [https://simd.jaytux.com](https://simd.jaytux.com/api)*
## Preparation
1. Set up the project's sources (recommended to just use Intellij IDEA, and have it figure out everything for you, which should work out of the box). Alternatively, the `./gradlew` wrapper can be used to build the project.
2. Create an environment file (`.env`) in the root directory of the project, with the following variables:
- `DATABASE_URL`: The JDBC URL to the database; for example:
- `jdbc:sqlite:./intrinsics.sqlite` for a local SQLite database
- `jdbc:mariadb://localhost:3306/intrinsics` for a MariaDB database (served on the local machine at port 3306), which should already be running
- `DATABASE_DRIVER`: The JDBC driver class name; for example:
- `org.sqlite.JDBC` for SQLite
- `org.mariadb.jdbc.Driver` for MariaDB
- `DATABASE_USER`: the username to connect to the database; not required for SQLite
- `DATABASE_PASSWORD`: the password to connect to the database; not required for SQLite
3. (Optional) change the default port in the `src/main/resources/application.yaml` configuration file.
4. Get the intrinsics data from the [Intel Intrinsics Guide](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html) and post-process the data (I am planning to automate this in the future, stand by).
1. The downloaded data is a ZIP-file, with the relevant data files being `files/data.js` and `files/perf2.js`.
2. Post-process `files/data.js` to be a valid XML file (when downloaded, it's an XML string in a JS variable):
1. Remove the `var data = "` prefix at the beginning of the file
2. Remove the `";` suffix at the end of the file
3. Replace all `\n` with actual newlines, and all `\"` with `"`, and then remove all trailing `\`
3. Post-process `files/perf2.js` to be a valid JSON file (when downloaded, it's a JSON string in a JS variable):
1. Remove the `perf2_js = ` prefix at the beginning of the file
2. Replace all `{l:` by `{"l":`, and all `,t:` by `,"t":`
5. At this point, you are ready to load the data into the database using the application. Run `./gradlew run -reload /path/to/post-processed/data.js /path/to/post-processed/perf2.js`
6. Finally, you can start the application using `./gradlew run`
## Running
By default, the application runs on port `42024`. You can start it using `./gradlew run`.
To reload the database (it drops all data, then re-parses everything), use `./gradlew run -reload /path/to/xml /path/to/json`.
## API
Most of the API is paginated. The default page size is 100, and the default page number is 0. Each paginated response has the following format:
```json
{
"page": <page number>,
"totalPages": <total number of pages>,
"items": []
}
```
Typically, you can use the "root" API endpoint (e.g. `/all`) to get the first page of results, and get any page by using the `/{page}` query parameter (e.g. `/all/3`). There is one notable exception (`/search`), where the page number is specified as a query parameter (`/search?page=3`). Finally, the details endpoint (`/details/{id}`) is not paginated, and returns a single object.
### `GET /all`
Gets a (paginated) list of all intrinsics. For each intrinsic, the following fields are returned:
```json
{
"id": "<UUID string>",
"name": "<function name of the intrinsic>"
}
```
Other pages can be requested using the `GET /all/{page}` endpoint (`GET /all` is equivalent to `GET /all/0`).
### `GET /cpuid`
Gets a (paginated) list of all CPUIDs in the database. Each CPUID can be used as a filter for the `/search` endpoint. The data is a simple list of strings. Examples include `PREFETCHI`, `SSE2`, `AVX`, etc.
### `GET /tech`
Gets a (paginated) list of all technologies in the database. Each technology can be used as a filter for the `/search` endpoint. The data is a simple list of strings. The full list (at the moment of writing) is:
- `AMX`
- `AVX-512`
- `AVX_ALL`
- `MMX`
- `Other`
- `SSE_ALL`
- `SVML`
### `GET /category`
Gets a (paginated) list of all categories in the database. Each category can be used as a filter for the `/search` endpoint. The data is a simple list of strings. Examples include `Logical`, `OS-Targeted`, `Swizzle`, etc.
### `GET /types`
Gets a (paginated) list of all C/C++(-like) types used by the intrinsics. The types can be used as filter for the `/search` endpoint (but currently only based on return type). The data is a simple list of strings. Examples include `__int16`, `__m128 const *`, `string literal`, etc.
### `GET /search`
Searches the database using the given filters. The filters are passed as query parameters, and can be combined. All filters are optional. The following filters are available:
- `name=[string]`: searches based on the name of the intrinsic; employs fuzzy-search (using `LIKE %it%`)
- `return=[string]`: searches based on the return type of the intrinsic; exact search only
- `cpuid=[string]`: searches based on the CPUID of the intrinsic; exact search only
- `tech=[string]`: searches based on the technology of the intrinsic; exact search only
- `category=[string]`: searches based on the category of the intrinsic; exact search only
- `desc=[string]`: searches based on the description of the intrinsic; employs fuzzy-search (using `LIKE %it%`)
- `page=[int]`: specifies the page number to return (default is 0)
Passing no filters is equivalent to using `GET /all`, and data is returned in the same format:
```json
{
"page": <page number>,
"totalPages": <total number of pages>,
"items": [
{
"id": "<UUID string>",
"name": "<function name of the intrinsic>"
}
]
}
```
### `GET /details/{id}`
Gets the details for a single, specific intrinsic. The following data is returned:
```json
{
"id": "<UUID string>",
"name": "<function name of the intrinsic>",
"returnType": "<return type of the intrinsic>",
"returnVar": "<variable name for the return value, as used in the description; can be null>",
"description": "<description of the intrinsic>",
"operations": "<operation of the intrinsic in pseudocode>",
"category": "<category>",
"cpuid": "<CPUID>",
"tech": "<technology>",
"params": [
{
"name": "<parameter name>",
"type": "<parameter type>"
}
],
"instructions": [
{
"mnemonic": "<instruction mnemonic>",
"xed": "<Intel XED code>",
"form": "<instruction argument form>"
}
],
"performance": [
{
"platform": "<platform name>",
"latency": <latency in cycles; can be null>,
"throughput": <throughput in CPI; can be null>
}
]
}
```

47
api/build.gradle.kts Normal file
View File

@ -0,0 +1,47 @@
plugins {
alias(libs.plugins.jvm)
alias(libs.plugins.ktor)
alias(libs.plugins.serialization)
}
group = "com.jaytux.simd"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.openapi)
implementation(libs.ktor.server.auto.head.response)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.config.yaml)
implementation(libs.ktor.server.test.host)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.dotenv)
implementation(libs.json)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ksoup)
implementation(libs.logback.classic)
implementation(libs.mariadb)
implementation(libs.sqlite)
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}

1
api/gradle.properties Normal file
View File

@ -0,0 +1 @@
kotlin.code.style=official

View File

@ -0,0 +1,46 @@
[versions]
kotlin = "2.1.10"
ktor = "3.1.2"
exposed = "0.58.0"
ksoup = "0.2.2"
logback = "1.5.6"
sqlite = "3.34.0"
mariadb = "3.5.3"
dotenv = "6.5.1"
serialization = "1.8.1"
json = "20231013"
[libraries]
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-openapi = { module = "io.ktor:ktor-server-openapi", version.ref = "ktor" }
ktor-server-auto-head-response = { module = "io.ktor:ktor-server-auto-head-response", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" }
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
dotenv = { module = "io.github.cdimascio:dotenv-kotlin", version.ref = "dotenv" }
json = { module = "org.json:json", version.ref = "json" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
mariadb = { module = "org.mariadb.jdbc:mariadb-java-client", version.ref = "mariadb" }
sqlite = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
[plugins]
jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

BIN
api/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
#Fri Apr 18 09:44:09 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
api/gradlew vendored Executable file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
api/gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

5
api/settings.gradle.kts Normal file
View File

@ -0,0 +1,5 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "api"

View File

@ -0,0 +1,15 @@
package com.jaytux.simd
import io.github.cdimascio.dotenv.dotenv
object DotEnv {
val env = dotenv()
operator fun get(name: String) = env[name]
class DotEnvException(missing: String) : RuntimeException("Missing environment variable: $missing") {
constructor(missing: String, cause: Throwable) : this(missing) {
initCause(cause)
}
}
}

View File

@ -0,0 +1,55 @@
package com.jaytux.simd
import com.jaytux.simd.data.Database
import com.jaytux.simd.data.Loader
import com.jaytux.simd.server.configureHTTP
import com.jaytux.simd.server.configureRouting
import com.jaytux.simd.server.configureSerialization
import io.ktor.server.application.*
import io.ktor.server.netty.*
import kotlinx.coroutines.runBlocking
fun main(args: Array<String>) {
if(args.size > 3 && args[1] == "-reload") {
val xmlFile = args[2]
val jsonFile = args[3]
dbSetup(xmlFile, jsonFile)
}
else if(args.size >= 1 && args[1] == "-h") {
println("Usage: ${args[0]} -reload <xmlFile> <jsonFile> (to reload the database)")
println(" ${args[0]} (to start the server)")
}
else {
EngineMain.main(args)
}
}
fun dbSetup(xmlFile: String, jsonFile: String) {
runBlocking {
val xml = Loader.loadXml("/home/jay/intrinsics/data.xml")
val perf = Loader.loadJson("/home/jay/intrinsics/perf2.js")
Loader.importToDb(xml, perf)
}
}
fun Application.module() {
Database.db
configureSerialization()
configureHTTP()
configureRouting()
}
// API: (everything except /details/ is paginated per 100)
// - GET /all (list of SIMD intrinsics (name + ID))
// - GET /cpuid (list of CPUID values)
// - GET /tech (list of techs)
// - GET /category (list of categories)
// - GET /types (list of types)
// - GET /search (search for SIMD intrinsics); query params:
// - name (string, optional, partial matching)
// - return (string, optional, full match)
// - cpuid (string, optional, full match)
// - tech (string, optional, full match)
// - category (string, optional, full match)
// - desc (string, optional, partial matching)
// - GET /details/<id> (details of a SIMD intrinsic)

View File

@ -0,0 +1,32 @@
package com.jaytux.simd.data
import com.jaytux.simd.DotEnv
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
object Database {
val db by lazy {
val db = Database.connect(
url = DotEnv["DATABASE_URL"] ?: throw DotEnv.DotEnvException("DATABASE_URL"),
driver = DotEnv["DATABASE_DRIVER"] ?: throw DotEnv.DotEnvException("DATABASE_DRIVER"),
user = DotEnv["DATABASE_USER"] ?: "",
password = DotEnv["DATABASE_PASSWORD"] ?: "",
)
transaction {
SchemaUtils.create(Techs, CppTypes, Categories, CPUIDs, Intrinsics,
IntrinsicArguments, IntrinsicInstructions, Platforms, Performances)
}
db
}
fun reset() {
transaction {
SchemaUtils.drop(Techs, CppTypes, Categories, CPUIDs, Intrinsics,
IntrinsicArguments, IntrinsicInstructions, Platforms, Performances)
SchemaUtils.create(Techs, CppTypes, Categories, CPUIDs, Intrinsics,
IntrinsicArguments, IntrinsicInstructions, Platforms, Performances)
}
}
}

View File

@ -0,0 +1,82 @@
package com.jaytux.simd.data
import com.jaytux.simd.data.CppType.Companion.optionalReferrersOn
import org.jetbrains.exposed.dao.*
import org.jetbrains.exposed.dao.id.CompositeID
import org.jetbrains.exposed.dao.id.EntityID
import java.util.*
class Tech(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Tech>(Techs)
var name by Techs.name
}
class CppType(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<CppType>(CppTypes)
var name by CppTypes.name
}
class Category(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Category>(Categories)
var name by Categories.name
}
class CPUID(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<CPUID>(CPUIDs)
var name by CPUIDs.name
}
class Intrinsic(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Intrinsic>(Intrinsics)
var mnemonic by Intrinsics.mnemonic
var returnType by CppType referencedOn Intrinsics.returnType
var returnVar by Intrinsics.returnVar
var description by Intrinsics.description
var operations by Intrinsics.operations
var category by Category referencedOn Intrinsics.category
var cpuid by CPUID optionalReferencedOn Intrinsics.cpuid
var tech by Tech referencedOn Intrinsics.tech
val arguments by IntrinsicArgument referrersOn IntrinsicArguments.intrinsic
val instructions by IntrinsicInstruction referrersOn IntrinsicInstructions.intrinsic
}
class IntrinsicArgument(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<IntrinsicArgument>(IntrinsicArguments)
var intrinsic by Intrinsic referencedOn IntrinsicArguments.intrinsic
var name by IntrinsicArguments.name
var type by CppType referencedOn IntrinsicArguments.type
var index by IntrinsicArguments.index
}
class IntrinsicInstruction(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<IntrinsicInstruction>(IntrinsicInstructions)
var intrinsic by Intrinsic referencedOn IntrinsicInstructions.intrinsic
var mnemonic by IntrinsicInstructions.mnemonic
var xed by IntrinsicInstructions.xed
var form by IntrinsicInstructions.form
val performance by Performance referrersOn Performances.instruction
}
class Platform(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Platform>(Platforms)
var name by Platforms.name
}
class Performance(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<Performance>(Performances)
var instruction by IntrinsicInstruction referencedOn Performances.instruction
var platform by Platform referencedOn Performances.platform
var latency by Performances.latency
var throughput by Performances.throughput
}

View File

@ -0,0 +1,205 @@
package com.jaytux.simd.data
import com.fleeksoft.ksoup.Ksoup
import com.jaytux.simd.data.IntrinsicInstructions.xed
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.transactions.transaction
import java.io.File
import org.json.JSONObject
object Loader {
data class XmlIntrinsic(val name: String, val tech: String, val retType: String, val retVar: String?, val args: List<Pair<String, String>>, val desc: String, val op: String?, val insn: List<Triple<String, String, String?>>, val cpuid: String?, val category: String)
data class XmlData(
val types: Set<String>, val techs: Set<String>, val cpuids: Set<String>, val categories: Set<String>,
val intrinsics: List<XmlIntrinsic>
)
data class Performance(val latency: Float?, val throughput: Float?)
data class JsonData(val platforms: Set<String>, val data: Map<String, Map<String, Performance>>)
suspend fun loadXml(xmlFile: String): XmlData = coroutineScope {
val xml = Ksoup.parseXml(File(xmlFile).readText(Charsets.UTF_8))
val cppTypes = mutableSetOf<String>()
val techs = mutableSetOf<String>()
val cpuids = mutableSetOf<String>()
val categories = mutableSetOf<String>()
val intrins = mutableListOf<XmlIntrinsic>()
val errors = mutableListOf<String>()
xml.getElementsByTag("intrinsic").forEachIndexed { i, it ->
val name = it.attribute("name")?.value
if(name == null) {
errors += "Missing name attribute in intrinsic element"
return@forEachIndexed
}
val tech = it.attribute("tech")?.value
if(tech == null) {
errors += "Missing tech attribute for intrinsic $name"
return@forEachIndexed
}
val ret = it.getElementsByTag("return").firstOrNull()
if(ret == null) {
errors += "Missing return element for intrinsic $name"
return@forEachIndexed
}
val retType = ret.attribute("type")?.value
if(retType == null) {
errors += "Missing type attribute for return element in intrinsic $name"
return@forEachIndexed
}
val retVar = ret.attribute("varname")?.value
val args = mutableListOf<Pair<String, String>>()
it.getElementsByTag("parameter").forEachIndexed { i, p ->
val argName = p.attribute("varname")?.value
val type = p.attribute("type")?.value
if(type != null && type == "void") return@forEachIndexed //ignore
if(argName == null) {
errors += "Missing varname attribute for parameter $i in intrinsic $name"
return@forEachIndexed
}
if(type == null) {
errors += "Missing type attribute for parameter $argName in intrinsic $name"
return@forEachIndexed
}
cppTypes += type
args += argName to type
}
val desc = it.getElementsByTag("description").firstOrNull()?.text()
if(desc == null) {
errors += "Missing description element for intrinsic $name"
return@forEachIndexed
}
val op = it.getElementsByTag("operation").firstOrNull()?.text()
val insn = mutableListOf<Triple<String, String, String?>>()
it.getElementsByTag("instruction").forEachIndexed { i, ins ->
val insnName = ins.attribute("xed")?.value ?: ins.attribute("name")?.value
if(insnName == null) {
errors += "Missing both xed and name attribute for instruction $i in intrinsic $name"
return@forEachIndexed
}
val insnMnemonic = ins.attribute("name")?.value
if(insnMnemonic == null) {
errors += "Missing name attribute for instruction $insnName in intrinsic $name"
return@forEachIndexed
}
val insnForm = ins.attribute("form")?.value
insn += Triple(insnName, insnMnemonic, insnForm)
}
val cpuid = it.getElementsByTag("cpuid").firstOrNull()?.text()
val category = it.getElementsByTag("category").firstOrNull()?.text()
if(category == null) {
errors += "Missing category element for intrinsic $name"
return@forEachIndexed
}
val intrinsic = XmlIntrinsic(name, tech, retType, retVar, args, desc, op, insn, cpuid, category)
intrins += intrinsic
techs += tech
cpuid?.let { c -> cpuids += c }
categories += category
cppTypes += retType
}
if(errors.isNotEmpty()) {
errors.forEach { System.err.println(it) }
throw Exception("XML file is (partially) invalid")
}
XmlData(types = cppTypes, techs = techs, cpuids = cpuids, categories = categories, intrinsics = intrins)
}
suspend fun loadJson(jsonFile: String): JsonData = coroutineScope {
val json = File(jsonFile).readText(Charsets.UTF_8)
val schema = JSONObject(json)
val pSet = mutableSetOf<String>()
val res = mutableMapOf<String, MutableMap<String, Performance>>()
schema.keys().forEach { opcode ->
val pMap = mutableMapOf<String, Performance>()
val platforms = schema.getJSONArray(opcode)
for (i in 0 until platforms.length()) {
val platform = platforms.getJSONObject(i)
platform.keys().forEach { k ->
pSet += k
val latency = platform.getJSONObject(k).getString("l").toFloatOrNull()
val throughput = platform.getJSONObject(k).getString("t").toFloatOrNull()
pMap += k to Performance(latency, throughput)
}
}
res += opcode to pMap
}
JsonData(pSet, res)
}
suspend fun importToDb(xml: XmlData, json: JsonData) = coroutineScope {
val db = Database.db
transaction {
val techMap = xml.techs.associateWith { tech -> Tech.new { name = tech } }
val typeMap = xml.types.associateWith { type -> CppType.new { name = type } }
val catMap = xml.categories.associateWith { cat -> Category.new { name = cat } }
val cpuidMap = xml.cpuids.associateWith { cpuid -> CPUID.new { name = cpuid } }
val platformMap = json.platforms.associateWith { platform -> Platform.new { name = platform } }
xml.intrinsics.forEach { intr ->
val dbIn = Intrinsic.new {
mnemonic = intr.name
returnType = typeMap[intr.retType] ?: throw Exception("Type ${intr.retType} not found")
returnVar = intr.retVar
description = intr.desc
operations = intr.op
category = catMap[intr.category] ?: throw Exception("Category ${intr.category} not found")
cpuid = intr.cpuid?.let { cpuidMap[it] ?: throw Exception("CPUID ${intr.cpuid} not found") }
tech = techMap[intr.tech] ?: throw Exception("Tech ${intr.tech} not found")
}
intr.args.forEachIndexed { i, arg ->
IntrinsicArgument.new {
intrinsic = dbIn
name = arg.first
type = typeMap[arg.second] ?: throw Exception("Type ${arg.second} not found")
index = i
}
}
intr.insn.forEach { insn ->
val dbInsn = IntrinsicInstruction.new {
intrinsic = dbIn
xed = insn.first
mnemonic = insn.second
insn.third?.let { form = it }
}
json.data[insn.first]?.forEach { (pl, perf) ->
val dbPl = platformMap[pl] ?: throw Exception("Platform $pl not found")
Performances.insert {
it[instruction] = dbInsn.id
it[platform] = dbPl.id
it[latency] = perf.latency
it[throughput] = perf.throughput
}
}
}
}
}
}
}

View File

@ -0,0 +1,63 @@
package com.jaytux.simd.data
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.dao.id.CompositeIdTable
object Techs : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object CppTypes : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object Categories : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object CPUIDs : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object Intrinsics : UUIDTable() {
val mnemonic = varchar("mnemonic", 255)
val returnType = reference("return_type", CppTypes)
val returnVar = varchar("return_var", 255).nullable()
val description = text("description")
val operations = text("operations").nullable()
val category = reference("category", Categories)
val cpuid = reference("cpuid", CPUIDs).nullable()
val tech = reference("tech", Techs)
}
object IntrinsicArguments : UUIDTable() {
val intrinsic = reference("intrinsic", Intrinsics)
val name = varchar("name", 255)
val type = reference("type", CppTypes)
val index = integer("index")
init {
uniqueIndex(intrinsic, name)
uniqueIndex(intrinsic, index)
}
}
object IntrinsicInstructions : UUIDTable() {
val intrinsic = reference("intrinsic", Intrinsics)
val mnemonic = varchar("mnemonic", 255)
val xed = varchar("xed", 255)
val form = text("form").nullable()
}
object Platforms : UUIDTable() {
val name = varchar("name", 255).uniqueIndex()
}
object Performances : CompositeIdTable() {
val instruction = reference("instruction", IntrinsicInstructions)
val platform = reference("platform", Platforms)
val latency = float("latency").nullable()
val throughput = float("throughput").nullable()
override val primaryKey: PrimaryKey = PrimaryKey(instruction, platform)
}

View File

@ -0,0 +1,122 @@
package com.jaytux.simd.server
import com.jaytux.simd.data.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
@Serializable
data class IntrinsicSummary(@Serializable(with = UUIDSerializer::class) val id: UUID, val name: String)
@Serializable
data class Param(val name: String, val type: String)
@Serializable
data class Instruction(val mnemonic: String, val xed: String, val form: String?)
@Serializable
data class PlatformPerformance(val platform: String, val latency: Float?, val throughput: Float?)
@Serializable
data class IntrinsicDetails(
@Serializable(with = UUIDSerializer::class) val id: UUID,
val name: String,
val returnType: String,
val returnVar: String?,
val description: String,
val operations: String?,
val category: String,
val cpuid: String?,
val tech: String,
val params: List<Param>,
val instructions: List<Instruction>?,
val performance: List<PlatformPerformance>?
)
fun Routing.installGetAll() {
getPagedUrl("/all", { 100 }, { IntrinsicSummary(it.id.value, it.mnemonic) }) {
Intrinsic.all().orderAsc(Intrinsics.mnemonic)
}
getPagedUrl("/cpuid", { 100 }, { it.name }) { CPUID.all().orderAsc(CPUIDs.name) }
getPagedUrl("/tech", { 100 }, { it.name }) { Tech.all().orderAsc(Techs.name) }
getPagedUrl("/category", { 100 }, { it.name }) { Category.all().orderAsc(Categories.name) }
getPagedUrl("/types", { 100 }, { it.name }) { CppType.all().orderAsc(CppTypes.name) }
}
fun Routing.installSearch() {
getPagedRequest("/search", { 100 }, { IntrinsicSummary(it[Intrinsics.id].value, it[Intrinsics.mnemonic]) }) {
val name = call.request.queryParameters["name"]
val returnType = call.request.queryParameters["return"]?.let {
CppType.find { CppTypes.name eq it }.firstOrNull()
?: throw HttpError("Unknown return type: $it")
}
val cpuid = call.request.queryParameters["cpuid"]?.let {
CPUID.find { CPUIDs.name eq it }.firstOrNull()
?: throw HttpError("Unknown CPUID: $it")
}
val tech = call.request.queryParameters["tech"]?.let {
Tech.find { Techs.name eq it }.firstOrNull()
?: throw HttpError("Unknown tech: $it")
}
val category = call.request.queryParameters["category"]?.let {
Category.find { Categories.name eq it }.firstOrNull()
?: throw HttpError("Unknown category: $it")
}
val desc = call.request.queryParameters["desc"]
var results = Intrinsics.selectAll()
name?.let { results = results.where { Intrinsics.mnemonic like "%$it%" } }
returnType?.let { results = results.where { Intrinsics.returnType eq it.id } }
cpuid?.let { results = results.where { Intrinsics.cpuid eq it.id } }
tech?.let { results = results.where { Intrinsics.tech eq it.id } }
category?.let { results = results.where { Intrinsics.category eq it.id } }
desc?.let { results = results.where { Intrinsics.description like "%$it%" } }
results.orderAsc(Intrinsics.mnemonic)
}
}
fun Routing.installDetails() {
get("/details/{id}") {
runCatching {
transaction {
val id = call.parameters["id"]?.let { UUID.fromString(it) }
?: throw HttpError("Missing or invalid ID")
val intrinsic = Intrinsic.findById(id)
?: throw HttpError("Unknown intrinsic: $id")
IntrinsicDetails(
id = intrinsic.id.value,
name = intrinsic.mnemonic,
returnType = intrinsic.returnType.name,
returnVar = intrinsic.returnVar,
description = intrinsic.description,
operations = intrinsic.operations,
category = intrinsic.category.name,
cpuid = intrinsic.cpuid?.name,
tech = intrinsic.tech.name,
params = intrinsic.arguments.orderAsc(IntrinsicArguments.index)
.map { Param(it.name, it.type.name) },
instructions = intrinsic.instructions.emptyToNull()
?.map { Instruction(it.mnemonic, it.xed, it.form) },
performance = intrinsic.instructions.firstOrNull()?.let {
(Performances innerJoin Platforms).selectAll().where {
Performances.instruction eq it.id
}.map {
PlatformPerformance(
platform = it[Platforms.name],
latency = it[Performances.latency],
throughput = it[Performances.throughput]
)
}
}
)
}
}
}
}

View File

@ -0,0 +1,10 @@
package com.jaytux.simd.server
import io.ktor.server.application.*
import io.ktor.server.routing.*
fun Application.configureHTTP() {
routing {
//
}
}

View File

@ -0,0 +1,18 @@
package com.jaytux.simd.server
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.Expression
import org.jetbrains.exposed.sql.SizedIterable
import org.jetbrains.exposed.sql.SortOrder
@Serializable data class Paginated<T>(val page: Long, val totalPages: Long, val items: List<T>)
inline fun <reified T, reified R> SizedIterable<T>.paginated(page: Long, perPage: Int, crossinline mapper: (T) -> R): Paginated<R> {
val total = this.count()
val subset = this.offset(page * perPage).limit(perPage).map { item -> mapper(item) }
return Paginated(page, total / perPage + if (total % perPage > 0) 1 else 0, subset)
}
fun <T> SizedIterable<T>.orderAsc(expr: Expression<*>) = this.orderBy(expr to SortOrder.ASC)
fun <T> SizedIterable<T>.emptyToNull() = if(empty()) null else this

View File

@ -0,0 +1,66 @@
package com.jaytux.simd.server
import com.jaytux.simd.data.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.autohead.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
@Serializable data class ErrorResponse(val error: String)
class HttpError(msg: String, val status: HttpStatusCode = HttpStatusCode.BadRequest) : Exception(msg)
inline suspend fun <reified R: Any> RoutingContext.runCatching(crossinline block: suspend RoutingContext.() -> R) {
try {
call.respond(block())
}
catch(err: HttpError) {
call.respond(err.status, ErrorResponse(err.message ?: "<no error message given>"))
}
}
inline fun <reified T, reified R> Route.getPagedUrl(
path: String,
crossinline perPage: RoutingContext.() -> Int,
crossinline mapper: (T) -> R,
crossinline getSet: RoutingContext.() -> SizedIterable<T>,
) {
get(path) {
runCatching {
transaction { getSet().paginated(0, perPage(), mapper) }
}
}
get("$path/{page}") {
runCatching {
val page = call.parameters["page"]?.toLongOrNull() ?: 0
transaction { getSet().paginated(page, perPage(), mapper) }
}
}
}
inline fun <reified T, reified R> Route.getPagedRequest(
path: String,
crossinline perPage: RoutingContext.() -> Int,
crossinline mapper: (T) -> R,
crossinline getSet: RoutingContext.() -> SizedIterable<T>,
) {
get(path) {
runCatching {
val page = call.request.queryParameters["page"]?.toLongOrNull() ?: 0
transaction { getSet().paginated(page, perPage(), mapper) }
}
}
}
fun Application.configureRouting() {
install(AutoHeadResponse)
routing {
installGetAll()
installSearch()
installDetails()
}
}

View File

@ -0,0 +1,28 @@
package com.jaytux.simd.server
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.routing.*
import io.ktor.util.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.Decoder
import java.util.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
object UUIDSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: UUID) = encoder.encodeString(value.toString())
}

View File

@ -0,0 +1,5 @@
ktor:
application:
modules: [ com.jaytux.simd.MainKt.module ]
deployment:
port: 42024

View File

@ -0,0 +1,10 @@
<configuration>
<appender name="APPENDER" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="APPENDER"/>
</root>
</configuration>