diff --git a/Dockerfile b/Dockerfile index 70048e9..721330c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,17 +5,6 @@ RUN dnf install -y gcc-c++ # For -fsanitize=undefined and -fsanitize=address RUN dnf install -y libasan libubsan -# Precompile bits/stdc++.h: https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html -# I believe flags like -Wall are ignored, but flags like -std, -O2, and -fsanitize=address must -# match the flags used to precompile the header. -RUN mkdir -p /precompiled-headers/bits/stdc++.h.gch -RUN g++ -std=c++11 -O2 -o /precompiled-headers/bits/stdc++.h.gch/01 /usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h -RUN g++ -std=c++17 -O2 -o /precompiled-headers/bits/stdc++.h.gch/02 /usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h -RUN g++ -std=c++23 -O2 -o /precompiled-headers/bits/stdc++.h.gch/03 /usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h -RUN g++ -std=c++11 -O2 -fsanitize=address -o /precompiled-headers/bits/stdc++.h.gch/04 /usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h -RUN g++ -std=c++17 -O2 -fsanitize=address -o /precompiled-headers/bits/stdc++.h.gch/05 /usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h -RUN g++ -std=c++23 -O2 -fsanitize=address -o /precompiled-headers/bits/stdc++.h.gch/06 /usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h - RUN dnf install -y java-21-amazon-corretto-devel RUN dnf install -y time diff --git a/README.md b/README.md index 8f3ec74..693b796 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,16 @@ Future updates: Maybe https://awscli.amazonaws.com/v2/documentation/api/latest/r Todo: - precompile `bits/stdc++.h` + +--- + + +```js +for (let i = 0; i < 100; i++) fetch("https://v3nuswv3poqzw6giv37wmrt6su0krxvt.lambda-url.us-east-1.on.aws/compile", { + method: "POST", + headers: {"Content-Type": "application/json" }, body: JSON.stringify({ + "source_code": "cat /proc/cpuinfo && sleep 1", + "compiler_options": "-O2 -std=c++17", + "language": "cpp" +}) }).then(x => x.json()).then(x => console.log(x.compile_output.stdout.match(/cpu MHz\t\t: (.*)/)[1])) +``` \ No newline at end of file diff --git a/src/compile.rs b/src/compile.rs index 783a453..465de7f 100644 --- a/src/compile.rs +++ b/src/compile.rs @@ -2,17 +2,20 @@ use std::{ fs::{self, File}, io::Write, os::unix::process::ExitStatusExt, - process::ExitStatus, + path::Path, + process::{Command, ExitStatus}, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use axum::Json; use base64::{prelude::BASE64_STANDARD, Engine}; use serde::{Deserialize, Serialize}; use tempdir::TempDir; use crate::{ - error::AppError, run_command::{run_command, CommandOptions, CommandOutput}, types::{Executable, Language} + error::AppError, + run_command::{run_command, CommandOptions, CommandOutput}, + types::{Executable, Language}, }; #[derive(Deserialize)] @@ -31,6 +34,60 @@ pub struct CompileResponse { pub compile_output: CommandOutput, } +/// Precompile bits/stdc++.h. +/// +/// Building bits/stdc++.h can be very slow. We can substantially speed this up by precompiling +/// headers: https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html +/// +/// However, precompiling headers is slow (~6s for C++23), and /tmp storage space is expensive, so +/// we only precompile bits/stdc++.h for some compiler options. +/// +/// I believe flags like -Wall are ignored, but flags like -std, -O2, and -fsanitize=address must +/// match the flags used to precompile the header. +/// +/// We don't do this precompilation in the dockerfile because lambda disk read speeds are abysmally +/// slow (~6 MB/s empirically), and the precompiled headers are quite large. +/// +/// We precompile headers even if the request doesn't need it. Otherwise if nobody uses C++23 for +/// example, one poor user may end up with long compile times for every lambda instance. By +/// precompiling headers for the first two requests, we reduce the chance that one user repeatedly +/// gets a slow experience. +fn precompile_headers() -> Result<()> { + const PRECOMPILE_VERSIONS: &'static [&'static str] = &["17", "23"]; + static mut VERSION_IDX: usize = 0; + + // Note: this must be single-threaded due to the use of static mut + if unsafe { VERSION_IDX } >= PRECOMPILE_VERSIONS.len() { + return Ok(()); + } + let cpp_version = PRECOMPILE_VERSIONS[unsafe { VERSION_IDX }]; + unsafe { VERSION_IDX += 1 }; + + let precompiled_header_path = + format!("/tmp/precompiled-headers/bits/stdc++.h.gch/{cpp_version}"); + + if Path::new(&precompiled_header_path).exists() { + return Ok(()); + } + + if !Command::new("g++") + .arg("-o") + .arg(precompiled_header_path) + .arg(format!("-std=c++{cpp_version}")) + .arg("-O2") + .arg("/usr/include/c++/11/x86_64-amazon-linux/bits/stdc++.h") + .status() + .with_context(|| format!("Failed to precompile header"))? + .success() + { + return Err(anyhow!( + "Command to precompile header exited with nonzero exit code" + )); + } + + Ok(()) +} + pub fn compile(compile_request: CompileRequest) -> Result { let tmp_dir = TempDir::new("compile")?; let tmp_out_dir = TempDir::new("compile-out")?; @@ -45,8 +102,12 @@ pub fn compile(compile_request: CompileRequest) -> Result { .into_string() .map_err(|_| anyhow!("failed to convert output_file_path into string"))?; + if let Err(err) = precompile_headers() { + println!("Warning: Failed to precompile headers: {err}"); + } + let command = format!( - "g++ -I/precompiled-headers -o {} {} program.cpp", + "g++ -I/tmp/precompiled-headers -o {} {} program.cpp", output_file_path, compile_request.compiler_options ); let compile_output = run_command( @@ -54,7 +115,7 @@ pub fn compile(compile_request: CompileRequest) -> Result { tmp_dir.path(), CommandOptions { stdin: String::new(), - timeout_ms: 5000, + timeout_ms: 10000, }, )?; diff --git a/src/main.rs b/src/main.rs index f1e0374..5f61cd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::fs; + use axum::{ routing::{get, post}, Json, Router }; @@ -22,6 +24,8 @@ async fn index_page() -> &'static str { async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); + fs::create_dir_all("/tmp/precompiled-headers/bits/stdc++.h.gch")?; + let app = Router::new() .route("/", get(index_page)) .route("/compile", post(compile_handler)) diff --git a/src/types.rs b/src/types.rs index be42e90..f0a15e6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq)] pub enum Language { #[serde(rename = "cpp")] Cpp,