Nikolay Rykunov
1 year ago
34 changed files with 1480 additions and 255 deletions
@ -0,0 +1,5 @@ |
|||||||
|
local.properties |
||||||
|
build |
||||||
|
.gradle |
||||||
|
.idea |
||||||
|
.DS_Store |
@ -0,0 +1,5 @@ |
|||||||
|
# Compose benchmarks for K/N |
||||||
|
|
||||||
|
## Run native on MacOS |
||||||
|
- `./gradlew runReleaseExecutableMacosArm64` (Works on Arm64 processors) |
||||||
|
- `./gradlew runReleaseExecutableMacosX64` (Works on Intel processors) |
@ -0,0 +1,83 @@ |
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile |
||||||
|
|
||||||
|
plugins { |
||||||
|
kotlin("multiplatform") |
||||||
|
id("org.jetbrains.compose") |
||||||
|
} |
||||||
|
|
||||||
|
version = "1.0-SNAPSHOT" |
||||||
|
|
||||||
|
repositories { |
||||||
|
mavenLocal() |
||||||
|
google() |
||||||
|
mavenCentral() |
||||||
|
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||||
|
} |
||||||
|
|
||||||
|
kotlin { |
||||||
|
jvm("desktop") |
||||||
|
macosX64 { |
||||||
|
binaries { |
||||||
|
executable { |
||||||
|
entryPoint = "main" |
||||||
|
freeCompilerArgs += listOf( |
||||||
|
"-linker-option", "-framework", "-linker-option", "Metal" |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
macosArm64 { |
||||||
|
binaries { |
||||||
|
executable { |
||||||
|
entryPoint = "main" |
||||||
|
freeCompilerArgs += listOf( |
||||||
|
"-linker-option", "-framework", "-linker-option", "Metal" |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
sourceSets { |
||||||
|
val commonMain by getting { |
||||||
|
dependencies { |
||||||
|
implementation(compose.ui) |
||||||
|
implementation(compose.foundation) |
||||||
|
implementation(compose.material) |
||||||
|
implementation(compose.runtime) |
||||||
|
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) |
||||||
|
implementation(compose.components.resources) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val desktopMain by getting { |
||||||
|
dependencies { |
||||||
|
implementation(compose.desktop.currentOs) |
||||||
|
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val nativeMain by creating { |
||||||
|
dependsOn(commonMain) |
||||||
|
} |
||||||
|
val macosMain by creating { |
||||||
|
dependsOn(nativeMain) |
||||||
|
} |
||||||
|
val macosX64Main by getting { |
||||||
|
dependsOn(macosMain) |
||||||
|
} |
||||||
|
val macosArm64Main by getting { |
||||||
|
dependsOn(macosMain) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
compose.desktop { |
||||||
|
application { |
||||||
|
mainClass = "Main_desktopKt" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
tasks.withType<KotlinCompile> { |
||||||
|
kotlinOptions.jvmTarget = "11" |
||||||
|
} |
||||||
|
|
@ -0,0 +1,10 @@ |
|||||||
|
compose.version=1.5.1 |
||||||
|
kotlin.version=1.9.10 |
||||||
|
org.gradle.jvmargs=-Xmx3g |
||||||
|
kotlin.native.useEmbeddableCompilerJar=true |
||||||
|
compose.desktop.verbose=true |
||||||
|
android.useAndroidX=true |
||||||
|
kotlin.js.webpack.major.version=4 |
||||||
|
org.jetbrains.compose.experimental.jscanvas.enabled=true |
||||||
|
org.jetbrains.compose.experimental.macos.enabled=true |
||||||
|
org.jetbrains.compose.experimental.uikit.enabled=true |
Binary file not shown.
@ -0,0 +1,5 @@ |
|||||||
|
distributionBase=GRADLE_USER_HOME |
||||||
|
distributionPath=wrapper/dists |
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip |
||||||
|
zipStoreBase=GRADLE_USER_HOME |
||||||
|
zipStorePath=wrapper/dists |
@ -0,0 +1,185 @@ |
|||||||
|
#!/usr/bin/env sh |
||||||
|
|
||||||
|
# |
||||||
|
# Copyright 2015 the original author or 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 UN*X |
||||||
|
## |
||||||
|
############################################################################## |
||||||
|
|
||||||
|
# Attempt to set APP_HOME |
||||||
|
# Resolve links: $0 may be a link |
||||||
|
PRG="$0" |
||||||
|
# Need this for relative symlinks. |
||||||
|
while [ -h "$PRG" ] ; do |
||||||
|
ls=`ls -ld "$PRG"` |
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'` |
||||||
|
if expr "$link" : '/.*' > /dev/null; then |
||||||
|
PRG="$link" |
||||||
|
else |
||||||
|
PRG=`dirname "$PRG"`"/$link" |
||||||
|
fi |
||||||
|
done |
||||||
|
SAVED="`pwd`" |
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null |
||||||
|
APP_HOME="`pwd -P`" |
||||||
|
cd "$SAVED" >/dev/null |
||||||
|
|
||||||
|
APP_NAME="Gradle" |
||||||
|
APP_BASE_NAME=`basename "$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 "$*" |
||||||
|
} |
||||||
|
|
||||||
|
die () { |
||||||
|
echo |
||||||
|
echo "$*" |
||||||
|
echo |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
|
||||||
|
# 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 |
||||||
|
;; |
||||||
|
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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then |
||||||
|
MAX_FD_LIMIT=`ulimit -H -n` |
||||||
|
if [ $? -eq 0 ] ; then |
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then |
||||||
|
MAX_FD="$MAX_FD_LIMIT" |
||||||
|
fi |
||||||
|
ulimit -n $MAX_FD |
||||||
|
if [ $? -ne 0 ] ; then |
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD" |
||||||
|
fi |
||||||
|
else |
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" |
||||||
|
fi |
||||||
|
fi |
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock |
||||||
|
if $darwin; then |
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" |
||||||
|
fi |
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java |
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then |
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"` |
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` |
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"` |
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath |
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` |
||||||
|
SEP="" |
||||||
|
for dir in $ROOTDIRSRAW ; do |
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir" |
||||||
|
SEP="|" |
||||||
|
done |
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))" |
||||||
|
# Add a user-defined pattern to the cygpath arguments |
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then |
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" |
||||||
|
fi |
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh |
||||||
|
i=0 |
||||||
|
for arg in "$@" ; do |
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` |
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option |
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition |
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` |
||||||
|
else |
||||||
|
eval `echo args$i`="\"$arg\"" |
||||||
|
fi |
||||||
|
i=`expr $i + 1` |
||||||
|
done |
||||||
|
case $i in |
||||||
|
0) set -- ;; |
||||||
|
1) set -- "$args0" ;; |
||||||
|
2) set -- "$args0" "$args1" ;; |
||||||
|
3) set -- "$args0" "$args1" "$args2" ;; |
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;; |
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; |
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; |
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; |
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; |
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; |
||||||
|
esac |
||||||
|
fi |
||||||
|
|
||||||
|
# Escape application args |
||||||
|
save () { |
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done |
||||||
|
echo " " |
||||||
|
} |
||||||
|
APP_ARGS=`save "$@"` |
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules |
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" |
||||||
|
|
||||||
|
exec "$JAVACMD" "$@" |
@ -0,0 +1,91 @@ |
|||||||
|
@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% equ 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% equ 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! |
||||||
|
set EXIT_CODE=%ERRORLEVEL% |
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1 |
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% |
||||||
|
exit /b %EXIT_CODE% |
||||||
|
|
||||||
|
:mainEnd |
||||||
|
if "%OS%"=="Windows_NT" endlocal |
||||||
|
|
||||||
|
:omega |
@ -0,0 +1,18 @@ |
|||||||
|
pluginManagement { |
||||||
|
repositories { |
||||||
|
mavenLocal() |
||||||
|
mavenCentral() |
||||||
|
gradlePluginPortal() |
||||||
|
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") |
||||||
|
google() |
||||||
|
} |
||||||
|
|
||||||
|
plugins { |
||||||
|
val kotlinVersion = extra["kotlin.version"] as String |
||||||
|
kotlin("multiplatform").version(kotlinVersion) |
||||||
|
val composeVersion = extra["compose.version"] as String |
||||||
|
id("org.jetbrains.compose").version(composeVersion) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
rootProject.name = "compose-benchmarks" |
@ -0,0 +1,151 @@ |
|||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import benchmarks.animation.AnimatedVisibility |
||||||
|
import benchmarks.lazygrid.LazyGrid |
||||||
|
import benchmarks.visualeffects.NYContent |
||||||
|
import kotlin.math.roundToInt |
||||||
|
import kotlin.time.Duration |
||||||
|
|
||||||
|
enum class BenchmarkFrameTimeKind { |
||||||
|
CPU, GPU |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
fun BenchmarkFrameTimeKind.toPrettyPrintString(): String = |
||||||
|
when (this) { |
||||||
|
BenchmarkFrameTimeKind.CPU -> "CPU" |
||||||
|
BenchmarkFrameTimeKind.GPU -> "GPU" |
||||||
|
} |
||||||
|
|
||||||
|
data class BenchmarkFrame( |
||||||
|
val cpuDuration: Duration, |
||||||
|
val gpuDuration: Duration |
||||||
|
) { |
||||||
|
fun duration(kind: BenchmarkFrameTimeKind): Duration = |
||||||
|
when (kind) { |
||||||
|
BenchmarkFrameTimeKind.CPU -> cpuDuration |
||||||
|
BenchmarkFrameTimeKind.GPU -> gpuDuration |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
data class BenchmarkPercentileAverage( |
||||||
|
val percentile: Double, |
||||||
|
val average: Duration |
||||||
|
) |
||||||
|
|
||||||
|
data class MissedFrames( |
||||||
|
val count: Int, |
||||||
|
val ratio: Double |
||||||
|
) { |
||||||
|
fun prettyPrint(description: String) { |
||||||
|
println( |
||||||
|
""" |
||||||
|
Missed frames ($description): |
||||||
|
- count: $count |
||||||
|
- ratio: $ratio |
||||||
|
|
||||||
|
""".trimIndent() |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
data class BenchmarkStats( |
||||||
|
val frameBudget: Duration, |
||||||
|
val percentileCPUAverage: List<BenchmarkPercentileAverage>, |
||||||
|
val percentileGPUAverage: List<BenchmarkPercentileAverage>, |
||||||
|
val noBufferingMissedFrames: MissedFrames, |
||||||
|
val doubleBufferingMissedFrames: MissedFrames |
||||||
|
) { |
||||||
|
fun prettyPrint() { |
||||||
|
percentileCPUAverage.prettyPrint(BenchmarkFrameTimeKind.CPU) |
||||||
|
println() |
||||||
|
percentileGPUAverage.prettyPrint(BenchmarkFrameTimeKind.GPU) |
||||||
|
println() |
||||||
|
noBufferingMissedFrames.prettyPrint("no buffering") |
||||||
|
doubleBufferingMissedFrames.prettyPrint("double buffering") |
||||||
|
} |
||||||
|
|
||||||
|
private fun List<BenchmarkPercentileAverage>.prettyPrint(kind: BenchmarkFrameTimeKind) { |
||||||
|
forEach { |
||||||
|
println("Worst p${(it.percentile * 100).roundToInt()} ${kind.toPrettyPrintString()} average: ${it.average}") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class BenchmarkResult( |
||||||
|
private val frameBudget: Duration, |
||||||
|
private val frames: List<BenchmarkFrame>, |
||||||
|
) { |
||||||
|
private fun percentileAverageFrameTime(percentile: Double, kind: BenchmarkFrameTimeKind): Duration { |
||||||
|
require(percentile in 0.0..1.0) |
||||||
|
|
||||||
|
val startIndex = ((frames.size - 1) * percentile).roundToInt() |
||||||
|
|
||||||
|
val percentileFrames = frames.sortedBy { it.duration(kind) }.subList(frames.size - startIndex - 1, frames.size) |
||||||
|
|
||||||
|
return averageDuration(percentileFrames) { |
||||||
|
it.duration(kind) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun generateStats(): BenchmarkStats { |
||||||
|
val noBufferingMissedFramesCount = frames.count { |
||||||
|
it.cpuDuration + it.gpuDuration > frameBudget |
||||||
|
} |
||||||
|
|
||||||
|
val doubleBufferingMissedFrames = frames.count { |
||||||
|
maxOf(it.cpuDuration, it.gpuDuration) > frameBudget |
||||||
|
} |
||||||
|
|
||||||
|
return BenchmarkStats( |
||||||
|
frameBudget, |
||||||
|
listOf(0.01, 0.02, 0.05, 0.1, 0.25, 0.5).map { percentile -> |
||||||
|
val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.CPU) |
||||||
|
|
||||||
|
BenchmarkPercentileAverage(percentile, average) |
||||||
|
}, |
||||||
|
listOf(0.01, 0.1, 0.5).map { percentile -> |
||||||
|
val average = percentileAverageFrameTime(percentile, BenchmarkFrameTimeKind.GPU) |
||||||
|
|
||||||
|
BenchmarkPercentileAverage(percentile, average) |
||||||
|
}, |
||||||
|
MissedFrames(noBufferingMissedFramesCount, noBufferingMissedFramesCount / frames.size.toDouble()), |
||||||
|
MissedFrames(doubleBufferingMissedFrames, doubleBufferingMissedFrames / frames.size.toDouble()) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
private fun averageDuration(frames: List<BenchmarkFrame>, selector: (BenchmarkFrame) -> Duration): Duration = |
||||||
|
frames.fold(Duration.ZERO) { acc, frame -> |
||||||
|
acc + selector(frame) |
||||||
|
} / frames.size |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
fun runBenchmark( |
||||||
|
name: String, |
||||||
|
width: Int, |
||||||
|
height: Int, |
||||||
|
targetFps: Int, |
||||||
|
frameCount: Int, |
||||||
|
graphicsContext: GraphicsContext?, |
||||||
|
content: @Composable () -> Unit |
||||||
|
): BenchmarkStats { |
||||||
|
val stats = measureComposable(frameCount, width, height, targetFps, graphicsContext, content).generateStats() |
||||||
|
|
||||||
|
println(name) |
||||||
|
stats.prettyPrint() |
||||||
|
println() |
||||||
|
|
||||||
|
return stats |
||||||
|
} |
||||||
|
|
||||||
|
fun runBenchmarks( |
||||||
|
width: Int = 1920, |
||||||
|
height: Int = 1080, |
||||||
|
targetFps: Int = 120, |
||||||
|
graphicsContext: GraphicsContext? = null |
||||||
|
) { |
||||||
|
println("Running emulating $targetFps FPS") |
||||||
|
runBenchmark("AnimatedVisibility", width, height, targetFps, 1000, graphicsContext) { AnimatedVisibility() } |
||||||
|
runBenchmark("LazyGrid", width, height, targetFps, 1000, graphicsContext) { LazyGrid() } |
||||||
|
runBenchmark("VisualEffects", width, height, targetFps, 1000, graphicsContext) { NYContent(width, height) } |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.ui.ComposeScene |
||||||
|
import androidx.compose.ui.unit.Constraints |
||||||
|
import org.jetbrains.skia.DirectContext |
||||||
|
import org.jetbrains.skia.Surface |
||||||
|
import kotlin.time.Duration |
||||||
|
import kotlin.time.Duration.Companion.nanoseconds |
||||||
|
import kotlin.time.ExperimentalTime |
||||||
|
import kotlin.time.measureTime |
||||||
|
import kotlinx.coroutines.* |
||||||
|
|
||||||
|
const val nanosPerSecond = 1E9.toLong() |
||||||
|
const val millisPerSecond = 1e3.toLong() |
||||||
|
const val nanosPerMillisecond = nanosPerSecond / millisPerSecond |
||||||
|
|
||||||
|
interface GraphicsContext { |
||||||
|
fun surface(width: Int, height: Int): Surface |
||||||
|
|
||||||
|
suspend fun awaitGPUCompletion() |
||||||
|
} |
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class) |
||||||
|
fun measureComposable( |
||||||
|
frameCount: Int = 500, |
||||||
|
width: Int, |
||||||
|
height: Int, |
||||||
|
targetFps: Int, |
||||||
|
graphicsContext: GraphicsContext?, |
||||||
|
content: @Composable () -> Unit |
||||||
|
): BenchmarkResult = runBlocking { |
||||||
|
val scene = ComposeScene() |
||||||
|
try { |
||||||
|
val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() |
||||||
|
scene.setContent(content) |
||||||
|
scene.constraints = Constraints.fixed(width, height) |
||||||
|
val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) |
||||||
|
|
||||||
|
val frames = MutableList(frameCount) { |
||||||
|
BenchmarkFrame(Duration.INFINITE, Duration.INFINITE) |
||||||
|
} |
||||||
|
|
||||||
|
var nanoTime = 0L |
||||||
|
|
||||||
|
repeat(frameCount) { |
||||||
|
val frameTime = measureTime { |
||||||
|
val cpuTime = measureTime { |
||||||
|
scene.render(surface.canvas, nanoTime) |
||||||
|
surface.flushAndSubmit(false) |
||||||
|
} |
||||||
|
|
||||||
|
val gpuTime = measureTime { |
||||||
|
graphicsContext?.awaitGPUCompletion() |
||||||
|
} |
||||||
|
|
||||||
|
frames[it] = BenchmarkFrame(cpuTime, gpuTime) |
||||||
|
} |
||||||
|
|
||||||
|
val actualNanosPerFrame = frameTime.inWholeNanoseconds |
||||||
|
val nanosUntilDeadline = nanosPerFrame - actualNanosPerFrame |
||||||
|
|
||||||
|
// Emulate waiting for next vsync |
||||||
|
if (nanosUntilDeadline > 0) { |
||||||
|
delay(nanosUntilDeadline / nanosPerMillisecond) |
||||||
|
} |
||||||
|
|
||||||
|
nanoTime += maxOf(actualNanosPerFrame, nanosPerFrame) |
||||||
|
} |
||||||
|
|
||||||
|
BenchmarkResult( |
||||||
|
nanosPerFrame.nanoseconds, |
||||||
|
frames |
||||||
|
) |
||||||
|
} finally { |
||||||
|
scene.close() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
package benchmarks.animation |
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility |
||||||
|
import androidx.compose.foundation.Image |
||||||
|
import androidx.compose.foundation.layout.Column |
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||||
|
import androidx.compose.material.MaterialTheme |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.LaunchedEffect |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.remember |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import androidx.compose.ui.Alignment |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import kotlinx.coroutines.delay |
||||||
|
import org.jetbrains.compose.resources.ExperimentalResourceApi |
||||||
|
import org.jetbrains.compose.resources.painterResource |
||||||
|
|
||||||
|
@OptIn(ExperimentalResourceApi::class) |
||||||
|
@Composable |
||||||
|
fun AnimatedVisibility() { |
||||||
|
MaterialTheme { |
||||||
|
var showImage by remember { mutableStateOf(false) } |
||||||
|
LaunchedEffect(showImage) { |
||||||
|
delay(200) |
||||||
|
showImage = !showImage |
||||||
|
} |
||||||
|
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { |
||||||
|
AnimatedVisibility(showImage) { |
||||||
|
Image( |
||||||
|
painterResource("compose-multiplatform.xml"), |
||||||
|
null |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,106 @@ |
|||||||
|
package benchmarks.lazygrid |
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy |
||||||
|
import androidx.compose.foundation.layout.Column |
||||||
|
import androidx.compose.foundation.layout.Row |
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth |
||||||
|
import androidx.compose.foundation.layout.padding |
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells |
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid |
||||||
|
import androidx.compose.foundation.lazy.grid.items |
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState |
||||||
|
import androidx.compose.material.Card |
||||||
|
import androidx.compose.material.Checkbox |
||||||
|
import androidx.compose.material.MaterialTheme |
||||||
|
import androidx.compose.material.Text |
||||||
|
import androidx.compose.runtime.Composable |
||||||
|
import androidx.compose.runtime.LaunchedEffect |
||||||
|
import androidx.compose.runtime.getValue |
||||||
|
import androidx.compose.runtime.mutableStateOf |
||||||
|
import androidx.compose.runtime.remember |
||||||
|
import androidx.compose.runtime.setValue |
||||||
|
import androidx.compose.runtime.withFrameMillis |
||||||
|
import androidx.compose.ui.Alignment |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.semantics.contentDescription |
||||||
|
import androidx.compose.ui.semantics.semantics |
||||||
|
import androidx.compose.ui.text.style.TextAlign |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun LazyGrid() { |
||||||
|
val itemCount = 12000 |
||||||
|
val entries = remember {List(itemCount) { Entry("$it") }} |
||||||
|
val state = rememberLazyGridState() |
||||||
|
|
||||||
|
var smoothScroll by remember { mutableStateOf(false)} |
||||||
|
|
||||||
|
MaterialTheme { |
||||||
|
Column { |
||||||
|
Row { |
||||||
|
Checkbox( |
||||||
|
checked = smoothScroll, |
||||||
|
onCheckedChange = { value -> smoothScroll = value} |
||||||
|
) |
||||||
|
Text (text = "Smooth scroll", modifier = Modifier.align(Alignment.CenterVertically)) |
||||||
|
} |
||||||
|
|
||||||
|
LazyVerticalGrid( |
||||||
|
columns = GridCells.Fixed(4), |
||||||
|
modifier = Modifier.fillMaxWidth().semantics { contentDescription = "IamLazy" }, |
||||||
|
state = state |
||||||
|
) { |
||||||
|
items(entries) { |
||||||
|
ListCell(it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
var curItem by remember { mutableStateOf(0) } |
||||||
|
var direct by remember { mutableStateOf(true) } |
||||||
|
if (smoothScroll) { |
||||||
|
LaunchedEffect(Unit) { |
||||||
|
while (smoothScroll) { |
||||||
|
withFrameMillis { } |
||||||
|
curItem = state.firstVisibleItemIndex |
||||||
|
if (curItem == 0) direct = true |
||||||
|
if (curItem > itemCount - 100) direct = false |
||||||
|
state.scrollBy(if (direct) 5f else -5f) |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
LaunchedEffect(curItem) { |
||||||
|
withFrameMillis { } |
||||||
|
curItem += if (direct) 50 else -50 |
||||||
|
if (curItem >= itemCount) { |
||||||
|
direct = false |
||||||
|
curItem = itemCount - 1 |
||||||
|
} else if (curItem <= 0) { |
||||||
|
direct = true |
||||||
|
curItem = 0 |
||||||
|
} |
||||||
|
state.scrollToItem(curItem) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
data class Entry(val contents: String) |
||||||
|
|
||||||
|
@Composable |
||||||
|
private fun ListCell(entry: Entry) { |
||||||
|
Card( |
||||||
|
modifier = Modifier |
||||||
|
.fillMaxWidth() |
||||||
|
.padding(8.dp) |
||||||
|
) { |
||||||
|
Text( |
||||||
|
text = entry.contents, |
||||||
|
textAlign = TextAlign.Center, |
||||||
|
style = MaterialTheme.typography.h5, |
||||||
|
modifier = Modifier.padding(16.dp) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,321 @@ |
|||||||
|
package benchmarks.visualeffects |
||||||
|
|
||||||
|
import androidx.compose.foundation.background |
||||||
|
import androidx.compose.foundation.layout.* |
||||||
|
import androidx.compose.foundation.shape.CircleShape |
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape |
||||||
|
import androidx.compose.material.Surface |
||||||
|
import androidx.compose.runtime.* |
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList |
||||||
|
import androidx.compose.ui.Modifier |
||||||
|
import androidx.compose.ui.draw.* |
||||||
|
import androidx.compose.ui.graphics.Color |
||||||
|
import androidx.compose.ui.platform.LocalDensity |
||||||
|
import androidx.compose.ui.unit.Dp |
||||||
|
import androidx.compose.ui.unit.dp |
||||||
|
|
||||||
|
import kotlin.math.* |
||||||
|
import kotlin.random.Random |
||||||
|
|
||||||
|
const val snowCount = 80 |
||||||
|
const val starCount = 60 |
||||||
|
const val rocketPartsCount = 30 |
||||||
|
|
||||||
|
data class SnowFlake( |
||||||
|
var x: Dp, |
||||||
|
var y: Dp, |
||||||
|
val scale: Float, |
||||||
|
var v: Double, |
||||||
|
var alpha: Float, |
||||||
|
var angle: Float, |
||||||
|
var rotate: Int, |
||||||
|
var phase: Double |
||||||
|
) |
||||||
|
|
||||||
|
data class Star(val x: Dp, val y: Dp, val color: Color, val size: Dp) |
||||||
|
|
||||||
|
val random = Random(123) |
||||||
|
|
||||||
|
fun random(): Float = random.nextFloat() |
||||||
|
|
||||||
|
class DoubleRocket(val particle: Particle) { |
||||||
|
private val STATE_ROCKET = 0 |
||||||
|
private val STATE_SMALL_ROCKETS = 1 |
||||||
|
var state = STATE_ROCKET |
||||||
|
var rockets: Array<Rocket> = emptyArray() |
||||||
|
private fun checkState(time: Long) { |
||||||
|
if (particle.vy > -3.0 && state == STATE_ROCKET) { |
||||||
|
explode(time) |
||||||
|
} |
||||||
|
if (state == STATE_SMALL_ROCKETS) { |
||||||
|
var done = true |
||||||
|
rockets.forEach { |
||||||
|
if (!it.exploded) { |
||||||
|
it.checkExplode(time) |
||||||
|
} |
||||||
|
if (!it.checkDone()) { |
||||||
|
done = false |
||||||
|
} |
||||||
|
} |
||||||
|
if (done) { |
||||||
|
reset() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun reset() { |
||||||
|
state = STATE_ROCKET |
||||||
|
particle.x = 0.0 |
||||||
|
particle.y = 1000.0 |
||||||
|
particle.vx = 2.1 |
||||||
|
particle.vy = -12.5 |
||||||
|
} |
||||||
|
|
||||||
|
private fun explode(time: Long) { |
||||||
|
val colors = arrayOf(Color(0xff, 0, 0), Color(192, 255, 192), Color(192, 212, 255)) |
||||||
|
rockets = Array(7) { |
||||||
|
val v = 1.2f + 1.0 * random() |
||||||
|
val angle = 2 * PI * random() |
||||||
|
Rocket( |
||||||
|
Particle( |
||||||
|
particle.x, |
||||||
|
particle.y, |
||||||
|
v * sin(angle) + particle.vx, |
||||||
|
v * cos(angle) + particle.vy - 0.5f, |
||||||
|
colors[it % colors.size] |
||||||
|
), colors[it % colors.size], time |
||||||
|
) |
||||||
|
} |
||||||
|
state = STATE_SMALL_ROCKETS |
||||||
|
} |
||||||
|
|
||||||
|
fun move(time: Long, prevTime: Long) { |
||||||
|
if (rocket.state == rocket.STATE_ROCKET) { |
||||||
|
rocket.particle.move(time, prevTime) |
||||||
|
rocket.particle.gravity(time, prevTime) |
||||||
|
} else { |
||||||
|
rocket.rockets.forEach { |
||||||
|
it.move(time, prevTime) |
||||||
|
} |
||||||
|
} |
||||||
|
rocket.checkState(time) |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun draw() { |
||||||
|
if (state == rocket.STATE_ROCKET) { |
||||||
|
particle.draw() |
||||||
|
} else { |
||||||
|
rockets.forEach { |
||||||
|
it.draw() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
class Rocket(val particle: Particle, val color: Color, val startTime: Long = 0) { |
||||||
|
var exploded = false |
||||||
|
var parts: Array<Particle> = emptyArray() |
||||||
|
|
||||||
|
fun checkExplode(time: Long) { |
||||||
|
if (time - startTime > 1200000000) { |
||||||
|
explode() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun explode() { |
||||||
|
parts = Array(rocketPartsCount) { |
||||||
|
val v = 0.5f + 1.5 * random() |
||||||
|
val angle = 2 * PI * random() |
||||||
|
Particle(particle.x, particle.y, v * sin(angle) + particle.vx, v * cos(angle) + particle.vy, color, 1) |
||||||
|
} |
||||||
|
exploded = true |
||||||
|
} |
||||||
|
|
||||||
|
fun checkDone(): Boolean { |
||||||
|
if (!exploded) return false |
||||||
|
parts.forEach { |
||||||
|
if (it.y < 800) return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
fun move(time: Long, prevTime: Long) { |
||||||
|
if (!exploded) { |
||||||
|
particle.move(time, prevTime) |
||||||
|
particle.gravity(time, prevTime) |
||||||
|
checkExplode(time) |
||||||
|
} else { |
||||||
|
parts.forEach { |
||||||
|
it.move(time, prevTime) |
||||||
|
it.gravity(time, prevTime) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun draw() { |
||||||
|
if (!exploded) { |
||||||
|
particle.draw() |
||||||
|
} else { |
||||||
|
parts.forEach { |
||||||
|
it.draw() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val color: Color, val type: Int = 0) { |
||||||
|
fun move(time: Long, prevTime: Long) { |
||||||
|
x = (x + vx * (time - prevTime) / 30000000) |
||||||
|
y = (y + vy * (time - prevTime) / 30000000) |
||||||
|
} |
||||||
|
|
||||||
|
fun gravity(time: Long, prevTime: Long) { |
||||||
|
vy = vy + 1.0f * (time - prevTime) / 300000000 |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun draw() { |
||||||
|
val alphaFactor = if (type == 0) 1.0f else 1 / (1 + abs(vy / 5)).toFloat() |
||||||
|
Box(Modifier.size(5.dp).offset(x.dp, y.dp).alpha(alphaFactor).clip(CircleShape).background(color)) |
||||||
|
for (i in 1..5) { |
||||||
|
Box( |
||||||
|
Modifier.size(4.dp).offset((x - vx / 2 * i).dp, (y - vy / 2 * i).dp) |
||||||
|
.alpha(alphaFactor * (1 - 0.18f * i)).clip(CircleShape).background(color) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
val rocket = DoubleRocket(Particle(0.0, 1000.0, 2.1, -12.5, Color.White)) |
||||||
|
|
||||||
|
fun prepareStarsAndSnowFlakes(stars: SnapshotStateList<Star>, snowFlakes: SnapshotStateList<SnowFlake>, width: Int, height: Int) { |
||||||
|
for (i in 0..snowCount) { |
||||||
|
snowFlakes.add( |
||||||
|
SnowFlake( |
||||||
|
(50 + (width - 50) * random()).dp, |
||||||
|
(height * random()).dp, |
||||||
|
0.1f + 0.2f * random(), |
||||||
|
1.5 + 3 * random(), |
||||||
|
(0.4f + 0.4 * random()).toFloat(), |
||||||
|
60 * random(), |
||||||
|
Random.nextInt(1, 5) - 3, |
||||||
|
random() * 2 * PI |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
val colors = arrayOf(Color.Red, Color.Yellow, Color.Green, Color.Yellow, Color.Cyan, Color.Magenta, Color.White) |
||||||
|
for (i in 0..starCount) { |
||||||
|
stars.add( |
||||||
|
Star( |
||||||
|
(width * random()).dp, |
||||||
|
(height * random()).dp, |
||||||
|
colors[Random.nextInt(colors.size)], |
||||||
|
(3 + 5 * random()).dp |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun NYContent(width: Int, height: Int) { |
||||||
|
var time by remember { mutableStateOf(0L) } |
||||||
|
val startTime = remember { 0L } |
||||||
|
var prevTime by remember { mutableStateOf(0L) } |
||||||
|
val snowFlakes = remember { mutableStateListOf<SnowFlake>() } |
||||||
|
val stars = remember { mutableStateListOf<Star>() } |
||||||
|
var flickering2 by remember { mutableStateOf(true) } |
||||||
|
remember { prepareStarsAndSnowFlakes(stars, snowFlakes, width, height) } |
||||||
|
|
||||||
|
Surface( |
||||||
|
modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)), |
||||||
|
color = Color.Black, |
||||||
|
shape = RoundedCornerShape(20.dp) |
||||||
|
) { |
||||||
|
|
||||||
|
LaunchedEffect(Unit) { |
||||||
|
while (true) { |
||||||
|
withFrameNanos { |
||||||
|
prevTime = time |
||||||
|
time = it |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (flickering2) { |
||||||
|
if (time - startTime > 15500000000) { //note, that startTime has been updated above |
||||||
|
flickering2 = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
rocket.move(time, prevTime) |
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) { |
||||||
|
|
||||||
|
snow(time, prevTime, snowFlakes, startTime, height) |
||||||
|
|
||||||
|
starrySky(stars) |
||||||
|
|
||||||
|
rocket.draw() |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Composable |
||||||
|
fun starrySky(stars: SnapshotStateList<Star>) { |
||||||
|
stars.forEach { |
||||||
|
star(it.x, it.y, it.color, size = it.size) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun star(x: Dp, y: Dp, color: Color = Color.White, size: Dp) { |
||||||
|
Box(Modifier.offset(x, y).scale(1.0f, 0.2f).rotate(45f).size(size).background(color)) |
||||||
|
Box(Modifier.offset(x, y).scale(0.2f, 1.0f).rotate(45f).size(size).background(color)) |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun snow(time: Long, prevTime: Long, snowFlakes: SnapshotStateList<SnowFlake>, startTime: Long, height: Int) { |
||||||
|
val deltaAngle = (time - startTime) / 100000000 |
||||||
|
with(LocalDensity.current) { |
||||||
|
snowFlakes.forEach { |
||||||
|
var y = it.y + ((it.v * (time - prevTime)) / 300000000).dp |
||||||
|
if (y > (height + 20).dp) { |
||||||
|
y = -20.dp |
||||||
|
} |
||||||
|
it.y = y |
||||||
|
val x = it.x + (15 * sin(time.toDouble() / 3000000000 + it.phase)).dp |
||||||
|
snowFlake(Modifier.offset(x, y).scale(it.scale).rotate(it.angle + deltaAngle * it.rotate), it.alpha) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun snowFlake(modifier: Modifier, alpha: Float = 0.8f) { |
||||||
|
Box(modifier) { |
||||||
|
snowFlakeInt(0, 0f, 30.dp, 0.dp, alpha) |
||||||
|
snowFlakeInt(0, 60f, 15.dp, 25.dp, alpha) |
||||||
|
snowFlakeInt(0, 120f, -15.dp, 25.dp, alpha) |
||||||
|
snowFlakeInt(0, 180f, -30.dp, 0.dp, alpha) |
||||||
|
snowFlakeInt(0, 240f, -15.dp, -25.dp, alpha) |
||||||
|
snowFlakeInt(0, 300f, 15.dp, -25.dp, alpha) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Composable |
||||||
|
fun snowFlakeInt(level: Int, angle: Float, shiftX: Dp, shiftY: Dp, alpha: Float) { |
||||||
|
if (level > 3) return |
||||||
|
Box( |
||||||
|
Modifier.offset(shiftX, shiftY).rotate(angle).width(100.dp).height(10.dp).scale(0.6f).alpha(1f) |
||||||
|
.background(Color.White.copy(alpha = alpha)) |
||||||
|
) { |
||||||
|
snowFlakeInt(level + 1, 30f, 12.dp, 20.dp, alpha * 0.8f) |
||||||
|
snowFlakeInt(level + 1, -30f, 12.dp, -20.dp, alpha * 0.8f) |
||||||
|
} |
||||||
|
} |
||||||
|
|
@ -0,0 +1,36 @@ |
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="600dp" |
||||||
|
android:height="600dp" |
||||||
|
android:viewportWidth="600" |
||||||
|
android:viewportHeight="600"> |
||||||
|
<path |
||||||
|
android:pathData="M301.21,418.53C300.97,418.54 300.73,418.56 300.49,418.56C297.09,418.59 293.74,417.72 290.79,416.05L222.6,377.54C220.63,376.43 219,374.82 217.85,372.88C216.7,370.94 216.09,368.73 216.07,366.47L216.07,288.16C216.06,287.32 216.09,286.49 216.17,285.67C216.38,283.54 216.91,281.5 217.71,279.6L199.29,268.27L177.74,256.19C175.72,260.43 174.73,265.23 174.78,270.22L174.79,387.05C174.85,393.89 178.57,400.2 184.53,403.56L286.26,461.02C290.67,463.51 295.66,464.8 300.73,464.76C300.91,464.76 301.09,464.74 301.27,464.74C301.24,449.84 301.22,439.23 301.22,439.23L301.21,418.53Z" |
||||||
|
android:fillColor="#041619" |
||||||
|
android:fillType="nonZero"/> |
||||||
|
<path |
||||||
|
android:pathData="M409.45,242.91L312.64,188.23C303.64,183.15 292.58,183.26 283.68,188.51L187.92,245C183.31,247.73 179.93,251.62 177.75,256.17L177.74,256.19L199.29,268.27L217.71,279.6C217.83,279.32 217.92,279.02 218.05,278.74C218.24,278.36 218.43,277.98 218.64,277.62C219.06,276.88 219.52,276.18 220.04,275.51C221.37,273.8 223.01,272.35 224.87,271.25L289.06,233.39C290.42,232.59 291.87,231.96 293.39,231.51C295.53,230.87 297.77,230.6 300,230.72C302.98,230.88 305.88,231.73 308.47,233.2L373.37,269.85C375.54,271.08 377.49,272.68 379.13,274.57C379.68,275.19 380.18,275.85 380.65,276.53C380.86,276.84 381.05,277.15 381.24,277.47L397.79,266.39L420.34,252.93L420.31,252.88C417.55,248.8 413.77,245.35 409.45,242.91Z" |
||||||
|
android:fillColor="#37BF6E" |
||||||
|
android:fillType="nonZero"/> |
||||||
|
<path |
||||||
|
android:pathData="M381.24,277.47C381.51,277.92 381.77,278.38 382.01,278.84C382.21,279.24 382.39,279.65 382.57,280.06C382.91,280.88 383.19,281.73 383.41,282.59C383.74,283.88 383.92,285.21 383.93,286.57L383.93,361.1C383.96,363.95 383.35,366.77 382.16,369.36C381.93,369.86 381.69,370.35 381.42,370.83C379.75,373.79 377.32,376.27 374.39,378L310.2,415.87C307.47,417.48 304.38,418.39 301.21,418.53L301.22,439.23C301.22,439.23 301.24,449.84 301.27,464.74C306.1,464.61 310.91,463.3 315.21,460.75L410.98,404.25C419.88,399 425.31,389.37 425.22,379.03L425.22,267.85C425.17,262.48 423.34,257.34 420.34,252.93L397.79,266.39L381.24,277.47Z" |
||||||
|
android:fillColor="#3870B2" |
||||||
|
android:fillType="nonZero"/> |
||||||
|
<path |
||||||
|
android:pathData="M177.75,256.17C179.93,251.62 183.31,247.73 187.92,245L283.68,188.51C292.58,183.26 303.64,183.15 312.64,188.23L409.45,242.91C413.77,245.35 417.55,248.8 420.31,252.88L420.34,252.93L498.59,206.19C494.03,199.46 487.79,193.78 480.67,189.75L320.86,99.49C306.01,91.1 287.75,91.27 273.07,99.95L114.99,193.2C107.39,197.69 101.81,204.11 98.21,211.63L177.74,256.19L177.75,256.17ZM301.27,464.74C301.09,464.74 300.91,464.76 300.73,464.76C295.66,464.8 290.67,463.51 286.26,461.02L184.53,403.56C178.57,400.2 174.85,393.89 174.79,387.05L174.78,270.22C174.73,265.23 175.72,260.43 177.74,256.19L98.21,211.63C94.86,218.63 93.23,226.58 93.31,234.82L93.31,427.67C93.42,438.97 99.54,449.37 109.4,454.92L277.31,549.77C284.6,553.88 292.84,556.01 301.2,555.94L301.2,555.8C301.39,543.78 301.33,495.26 301.27,464.74Z" |
||||||
|
android:strokeWidth="10" |
||||||
|
android:fillColor="#00000000" |
||||||
|
android:strokeColor="#083042" |
||||||
|
android:fillType="nonZero"/> |
||||||
|
<path |
||||||
|
android:pathData="M498.59,206.19L420.34,252.93C423.34,257.34 425.17,262.48 425.22,267.85L425.22,379.03C425.31,389.37 419.88,399 410.98,404.25L315.21,460.75C310.91,463.3 306.1,464.61 301.27,464.74C301.33,495.26 301.39,543.78 301.2,555.8L301.2,555.94C309.48,555.87 317.74,553.68 325.11,549.32L483.18,456.06C497.87,447.39 506.85,431.49 506.69,414.43L506.69,230.91C506.6,222.02 503.57,213.5 498.59,206.19Z" |
||||||
|
android:strokeWidth="10" |
||||||
|
android:fillColor="#00000000" |
||||||
|
android:strokeColor="#083042" |
||||||
|
android:fillType="nonZero"/> |
||||||
|
<path |
||||||
|
android:pathData="M301.2,555.94C292.84,556.01 284.6,553.88 277.31,549.76L109.4,454.92C99.54,449.37 93.42,438.97 93.31,427.67L93.31,234.82C93.23,226.58 94.86,218.63 98.21,211.63C101.81,204.11 107.39,197.69 114.99,193.2L273.07,99.95C287.75,91.27 306.01,91.1 320.86,99.49L480.67,189.75C487.79,193.78 494.03,199.46 498.59,206.19C503.57,213.5 506.6,222.02 506.69,230.91L506.69,414.43C506.85,431.49 497.87,447.39 483.18,456.06L325.11,549.32C317.74,553.68 309.48,555.87 301.2,555.94Z" |
||||||
|
android:strokeWidth="10" |
||||||
|
android:fillColor="#00000000" |
||||||
|
android:strokeColor="#083042" |
||||||
|
android:fillType="nonZero"/> |
||||||
|
</vector> |
@ -0,0 +1,9 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. |
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||||
|
*/ |
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers |
||||||
|
import kotlinx.coroutines.runBlocking |
||||||
|
|
||||||
|
fun main() = runBlocking(Dispatchers.Main) { runBenchmarks() } |
@ -0,0 +1,66 @@ |
|||||||
|
import kotlinx.cinterop.ExperimentalForeignApi |
||||||
|
import kotlinx.cinterop.objcPtr |
||||||
|
import org.jetbrains.skia.* |
||||||
|
import platform.Metal.* |
||||||
|
import kotlin.coroutines.resume |
||||||
|
import kotlin.coroutines.suspendCoroutine |
||||||
|
|
||||||
|
/* |
||||||
|
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. |
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||||
|
*/ |
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class) |
||||||
|
fun main() { |
||||||
|
val graphicsContext = object : GraphicsContext { |
||||||
|
private val device = MTLCreateSystemDefaultDevice() ?: throw IllegalStateException("Can't create MTLDevice") |
||||||
|
private val commandQueue = device.newCommandQueue() ?: throw IllegalStateException("Can't create MTLCommandQueue") |
||||||
|
private val directContext = DirectContext.makeMetal(device.objcPtr(), commandQueue.objcPtr()) |
||||||
|
private var cachedSurface: Surface? = null |
||||||
|
|
||||||
|
override fun surface(width: Int, height: Int): Surface { |
||||||
|
val oldSurface = cachedSurface |
||||||
|
|
||||||
|
if (oldSurface != null && oldSurface.width == width && oldSurface.height == height) { |
||||||
|
return oldSurface |
||||||
|
} |
||||||
|
|
||||||
|
val descriptor = MTLTextureDescriptor() |
||||||
|
descriptor.width = width.toULong() |
||||||
|
descriptor.height = height.toULong() |
||||||
|
descriptor.usage = MTLTextureUsageShaderRead or MTLTextureUsageShaderWrite or MTLTextureUsageRenderTarget |
||||||
|
descriptor.textureType = MTLTextureType2D |
||||||
|
descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm |
||||||
|
descriptor.mipmapLevelCount = 1UL |
||||||
|
|
||||||
|
val texture = device.newTextureWithDescriptor(descriptor) ?: throw IllegalStateException("Can't create MTLTexture") |
||||||
|
|
||||||
|
val renderTarget = BackendRenderTarget.makeMetal(width, height, texture.objcPtr()) |
||||||
|
|
||||||
|
return Surface.makeFromBackendRenderTarget( |
||||||
|
directContext, |
||||||
|
renderTarget, |
||||||
|
SurfaceOrigin.TOP_LEFT, |
||||||
|
SurfaceColorFormat.BGRA_8888, |
||||||
|
ColorSpace.sRGB, |
||||||
|
SurfaceProps(pixelGeometry = PixelGeometry.UNKNOWN) |
||||||
|
).also { |
||||||
|
cachedSurface = it |
||||||
|
} ?: throw IllegalStateException("Can't create Surface") |
||||||
|
} |
||||||
|
|
||||||
|
override suspend fun awaitGPUCompletion() { |
||||||
|
val commandBuffer = commandQueue.commandBuffer() ?: return |
||||||
|
|
||||||
|
suspendCoroutine { continuation -> |
||||||
|
commandBuffer.addCompletedHandler { |
||||||
|
continuation.resume(Unit) |
||||||
|
} |
||||||
|
|
||||||
|
commandBuffer.commit() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
runBenchmarks(graphicsContext = graphicsContext) |
||||||
|
} |
@ -1,60 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. |
|
||||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
|
||||||
*/ |
|
||||||
|
|
||||||
package org.jetbrains.compose.desktop.application.tasks |
|
||||||
|
|
||||||
import org.gradle.api.file.DirectoryProperty |
|
||||||
import org.gradle.api.tasks.* |
|
||||||
import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner |
|
||||||
import org.jetbrains.compose.internal.utils.MacUtils |
|
||||||
import org.jetbrains.compose.desktop.application.internal.NOTARIZATION_REQUEST_INFO_FILE_NAME |
|
||||||
import org.jetbrains.compose.desktop.application.internal.NotarizationRequestInfo |
|
||||||
import org.jetbrains.compose.internal.utils.ioFile |
|
||||||
|
|
||||||
abstract class AbstractCheckNotarizationStatusTask : AbstractNotarizationTask() { |
|
||||||
@get:Internal |
|
||||||
val requestDir: DirectoryProperty = objects.directoryProperty() |
|
||||||
|
|
||||||
@TaskAction |
|
||||||
fun run() { |
|
||||||
val notarization = validateNotarization() |
|
||||||
|
|
||||||
val requests = HashSet<NotarizationRequestInfo>() |
|
||||||
for (file in requestDir.ioFile.walk()) { |
|
||||||
if (file.isFile && file.name == NOTARIZATION_REQUEST_INFO_FILE_NAME) { |
|
||||||
try { |
|
||||||
val status = NotarizationRequestInfo() |
|
||||||
status.loadFrom(file) |
|
||||||
requests.add(status) |
|
||||||
} catch (e: Exception) { |
|
||||||
logger.error("Invalid notarization request status file: $file", e) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (requests.isEmpty()) { |
|
||||||
logger.quiet("No existing notarization requests") |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
for (request in requests.sortedBy { it.uploadTime }) { |
|
||||||
try { |
|
||||||
logger.quiet("Checking status of notarization request '${request.uuid}'") |
|
||||||
runExternalTool( |
|
||||||
tool = MacUtils.xcrun, |
|
||||||
args = listOf( |
|
||||||
"altool", |
|
||||||
"--notarization-info", request.uuid, |
|
||||||
"--username", notarization.appleID, |
|
||||||
"--password", notarization.password |
|
||||||
), |
|
||||||
logToConsole = ExternalToolRunner.LogToConsole.Always |
|
||||||
) |
|
||||||
} catch (e: Exception) { |
|
||||||
logger.error("Could not check notarization request '${request.uuid}'", e) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,82 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. |
|
||||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
|
||||||
*/ |
|
||||||
|
|
||||||
package org.jetbrains.compose.desktop.application.tasks |
|
||||||
|
|
||||||
import org.gradle.api.file.DirectoryProperty |
|
||||||
import org.gradle.api.tasks.* |
|
||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
|
||||||
import org.jetbrains.compose.desktop.application.internal.* |
|
||||||
import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile |
|
||||||
import org.jetbrains.compose.desktop.application.internal.files.findOutputFileOrDir |
|
||||||
import org.jetbrains.compose.internal.utils.MacUtils |
|
||||||
import org.jetbrains.compose.internal.utils.ioFile |
|
||||||
import java.io.File |
|
||||||
import java.time.LocalDateTime |
|
||||||
import java.time.format.DateTimeFormatter |
|
||||||
import javax.inject.Inject |
|
||||||
|
|
||||||
abstract class AbstractUploadAppForNotarizationTask @Inject constructor( |
|
||||||
@get:Input |
|
||||||
val targetFormat: TargetFormat |
|
||||||
) : AbstractNotarizationTask() { |
|
||||||
@get:InputDirectory |
|
||||||
val inputDir: DirectoryProperty = objects.directoryProperty() |
|
||||||
|
|
||||||
@get:Internal |
|
||||||
val requestsDir: DirectoryProperty = objects.directoryProperty() |
|
||||||
|
|
||||||
init { |
|
||||||
check(targetFormat != TargetFormat.AppImage) { "${TargetFormat.AppImage} cannot be notarized!" } |
|
||||||
} |
|
||||||
|
|
||||||
@TaskAction |
|
||||||
fun run() { |
|
||||||
val notarization = validateNotarization() |
|
||||||
val packageFile = findOutputFileOrDir(inputDir.ioFile, targetFormat).checkExistingFile() |
|
||||||
|
|
||||||
logger.quiet("Uploading '${packageFile.name}' for notarization (package id: '${notarization.bundleID}')") |
|
||||||
val args = arrayListOf( |
|
||||||
"altool", |
|
||||||
"--notarize-app", |
|
||||||
"--primary-bundle-id", notarization.bundleID, |
|
||||||
"--username", notarization.appleID, |
|
||||||
"--password", notarization.password, |
|
||||||
"--file", packageFile.absolutePath |
|
||||||
) |
|
||||||
if (notarization.ascProvider != null) { |
|
||||||
args.add("--asc-provider") |
|
||||||
args.add(notarization.ascProvider) |
|
||||||
} |
|
||||||
|
|
||||||
runExternalTool( |
|
||||||
tool = MacUtils.xcrun, |
|
||||||
args = args, |
|
||||||
processStdout = { output -> |
|
||||||
processUploadToolOutput(packageFile, output) |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
private fun processUploadToolOutput(packageFile: File, output: String) { |
|
||||||
val m = "RequestUUID = ([A-Za-z0-9\\-]+)".toRegex().find(output) |
|
||||||
?: error("Could not determine RequestUUID from output: $output") |
|
||||||
|
|
||||||
val requestId = m.groupValues[1] |
|
||||||
|
|
||||||
val uploadTime = LocalDateTime.now() |
|
||||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) |
|
||||||
val requestDir = requestsDir.ioFile.resolve("$uploadTime-${targetFormat.id}") |
|
||||||
val packageCopy = requestDir.resolve(packageFile.name) |
|
||||||
packageFile.copyTo(packageCopy) |
|
||||||
val requestInfo = NotarizationRequestInfo(uuid = requestId, uploadTime = uploadTime) |
|
||||||
val requestInfoFile = requestDir.resolve(NOTARIZATION_REQUEST_INFO_FILE_NAME) |
|
||||||
requestInfo.saveTo(requestInfoFile) |
|
||||||
|
|
||||||
logger.quiet("Request UUID: $requestId") |
|
||||||
logger.quiet("Request UUID is saved to ${requestInfoFile.absolutePath}") |
|
||||||
logger.quiet("Uploaded file is saved to ${packageCopy.absolutePath}") |
|
||||||
} |
|
||||||
} |
|
After Width: | Height: | Size: 49 KiB |
Loading…
Reference in new issue