Merge "aconfig: rename subcommand 'dump' -> 'dump-cache'" into main
diff --git a/tools/aconfig/Android.bp b/tools/aconfig/Android.bp
index 82bfa7e..0e3c37c 100644
--- a/tools/aconfig/Android.bp
+++ b/tools/aconfig/Android.bp
@@ -174,6 +174,12 @@
mode: "test",
}
+cc_aconfig_library {
+ name: "aconfig_test_cpp_library_exported_variant",
+ aconfig_declarations: "aconfig.test.flags",
+ mode: "exported",
+}
+
cc_test {
name: "aconfig.test.cpp",
srcs: [
@@ -204,6 +210,21 @@
test_suites: ["general-tests"],
}
+cc_test {
+ name: "aconfig.test.cpp.exported_mode",
+ srcs: [
+ "tests/aconfig_exported_mode_test.cpp",
+ ],
+ static_libs: [
+ "aconfig_test_cpp_library_exported_variant",
+ "libgmock",
+ ],
+ shared_libs: [
+ "server_configurable_flags",
+ ],
+ test_suites: ["general-tests"],
+}
+
rust_aconfig_library {
name: "libaconfig_test_rust_library",
crate_name: "aconfig_test_rust_library",
@@ -238,3 +259,21 @@
],
test_suites: ["general-tests"],
}
+
+rust_aconfig_library {
+ name: "libaconfig_test_rust_library_with_exported_mode",
+ crate_name: "aconfig_test_rust_library",
+ aconfig_declarations: "aconfig.test.flags",
+ mode: "exported",
+}
+
+rust_test {
+ name: "aconfig.exported_mode.test.rust",
+ srcs: [
+ "tests/aconfig_exported_mode_test.rs"
+ ],
+ rustlibs: [
+ "libaconfig_test_rust_library_with_exported_mode",
+ ],
+ test_suites: ["general-tests"],
+}
diff --git a/tools/aconfig/src/codegen/cpp.rs b/tools/aconfig/src/codegen/cpp.rs
index 568302d..06e5cca 100644
--- a/tools/aconfig/src/codegen/cpp.rs
+++ b/tools/aconfig/src/codegen/cpp.rs
@@ -20,7 +20,8 @@
use tinytemplate::TinyTemplate;
use crate::codegen;
-use crate::commands::{CodegenMode, OutputFile};
+use crate::codegen::CodegenMode;
+use crate::commands::OutputFile;
use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
pub fn generate_cpp_code<I>(
@@ -49,7 +50,9 @@
has_fixed_read_only,
readwrite,
readwrite_count,
- for_test: codegen_mode == CodegenMode::Test,
+ is_test_mode: codegen_mode == CodegenMode::Test,
+ is_prod_mode: codegen_mode == CodegenMode::Production,
+ is_exported_mode: codegen_mode == CodegenMode::Exported,
class_elements,
};
@@ -92,7 +95,9 @@
pub has_fixed_read_only: bool,
pub readwrite: bool,
pub readwrite_count: i32,
- pub for_test: bool,
+ pub is_test_mode: bool,
+ pub is_prod_mode: bool,
+ pub is_exported_mode: bool,
pub class_elements: Vec<ClassElement>,
}
@@ -149,6 +154,10 @@
#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO true
#endif
+#ifndef COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED
+#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED true
+#endif
+
#ifdef __cplusplus
#include <memory>
@@ -169,6 +178,8 @@
virtual bool enabled_fixed_ro() = 0;
+ virtual bool enabled_fixed_ro_exported() = 0;
+
virtual bool enabled_ro() = 0;
virtual bool enabled_ro_exported() = 0;
@@ -198,6 +209,10 @@
return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
}
+inline bool enabled_fixed_ro_exported() {
+ return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED;
+}
+
inline bool enabled_ro() {
return true;
}
@@ -225,6 +240,8 @@
bool com_android_aconfig_test_enabled_fixed_ro();
+bool com_android_aconfig_test_enabled_fixed_ro_exported();
+
bool com_android_aconfig_test_enabled_ro();
bool com_android_aconfig_test_enabled_ro_exported();
@@ -270,6 +287,10 @@
virtual void enabled_fixed_ro(bool val) = 0;
+ virtual bool enabled_fixed_ro_exported() = 0;
+
+ virtual void enabled_fixed_ro_exported(bool val) = 0;
+
virtual bool enabled_ro() = 0;
virtual void enabled_ro(bool val) = 0;
@@ -327,6 +348,14 @@
provider_->enabled_fixed_ro(val);
}
+inline bool enabled_fixed_ro_exported() {
+ return provider_->enabled_fixed_ro_exported();
+}
+
+inline void enabled_fixed_ro_exported(bool val) {
+ provider_->enabled_fixed_ro_exported(val);
+}
+
inline bool enabled_ro() {
return provider_->enabled_ro();
}
@@ -380,6 +409,10 @@
void set_com_android_aconfig_test_enabled_fixed_ro(bool val);
+bool com_android_aconfig_test_enabled_fixed_ro_exported();
+
+void set_com_android_aconfig_test_enabled_fixed_ro_exported(bool val);
+
bool com_android_aconfig_test_enabled_ro();
void set_com_android_aconfig_test_enabled_ro(bool val);
@@ -402,6 +435,56 @@
"#;
+ const EXPORTED_EXPORTED_HEADER_EXPECTED: &str = r#"
+#pragma once
+
+#ifdef __cplusplus
+
+#include <memory>
+
+namespace com::android::aconfig::test {
+
+class flag_provider_interface {
+public:
+ virtual ~flag_provider_interface() = default;
+
+ virtual bool disabled_rw_exported() = 0;
+
+ virtual bool enabled_fixed_ro_exported() = 0;
+
+ virtual bool enabled_ro_exported() = 0;
+};
+
+extern std::unique_ptr<flag_provider_interface> provider_;
+
+inline bool disabled_rw_exported() {
+ return provider_->disabled_rw_exported();
+}
+
+inline bool enabled_fixed_ro_exported() {
+ return provider_->enabled_fixed_ro_exported();
+}
+
+inline bool enabled_ro_exported() {
+ return provider_->enabled_ro_exported();
+}
+
+}
+
+extern "C" {
+#endif // __cplusplus
+
+bool com_android_aconfig_test_disabled_rw_exported();
+
+bool com_android_aconfig_test_enabled_fixed_ro_exported();
+
+bool com_android_aconfig_test_enabled_ro_exported();
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+"#;
+
const PROD_SOURCE_FILE_EXPECTED: &str = r#"
#include "com_android_aconfig_test.h"
#include <server_configurable_flags/get_flags.h>
@@ -450,6 +533,10 @@
return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
}
+ virtual bool enabled_fixed_ro_exported() override {
+ return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED;
+ }
+
virtual bool enabled_ro() override {
return true;
}
@@ -496,6 +583,10 @@
return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
}
+bool com_android_aconfig_test_enabled_fixed_ro_exported() {
+ return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO_EXPORTED;
+}
+
bool com_android_aconfig_test_enabled_ro() {
return true;
}
@@ -601,6 +692,19 @@
overrides_["enabled_fixed_ro"] = val;
}
+ virtual bool enabled_fixed_ro_exported() override {
+ auto it = overrides_.find("enabled_fixed_ro_exported");
+ if (it != overrides_.end()) {
+ return it->second;
+ } else {
+ return true;
+ }
+ }
+
+ virtual void enabled_fixed_ro_exported(bool val) override {
+ overrides_["enabled_fixed_ro_exported"] = val;
+ }
+
virtual bool enabled_ro() override {
auto it = overrides_.find("enabled_ro");
if (it != overrides_.end()) {
@@ -697,6 +801,13 @@
com::android::aconfig::test::enabled_fixed_ro(val);
}
+bool com_android_aconfig_test_enabled_fixed_ro_exported() {
+ return com::android::aconfig::test::enabled_fixed_ro_exported();
+}
+
+void set_com_android_aconfig_test_enabled_fixed_ro_exported(bool val) {
+ com::android::aconfig::test::enabled_fixed_ro_exported(val);
+}
bool com_android_aconfig_test_enabled_ro() {
return com::android::aconfig::test::enabled_ro();
@@ -733,6 +844,68 @@
"#;
+ const EXPORTED_SOURCE_FILE_EXPECTED: &str = r#"
+#include "com_android_aconfig_test.h"
+#include <server_configurable_flags/get_flags.h>
+#include <vector>
+
+namespace com::android::aconfig::test {
+
+ class flag_provider : public flag_provider_interface {
+ public:
+ virtual bool disabled_rw_exported() override {
+ if (cache_[0] == -1) {
+ cache_[0] = server_configurable_flags::GetServerConfigurableFlag(
+ "aconfig_flags.aconfig_test",
+ "com.android.aconfig.test.disabled_rw_exported",
+ "false") == "true";
+ }
+ return cache_[0];
+ }
+
+ virtual bool enabled_fixed_ro_exported() override {
+ if (cache_[1] == -1) {
+ cache_[1] = server_configurable_flags::GetServerConfigurableFlag(
+ "aconfig_flags.aconfig_test",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
+ "false") == "true";
+ }
+ return cache_[1];
+ }
+
+ virtual bool enabled_ro_exported() override {
+ if (cache_[2] == -1) {
+ cache_[2] = server_configurable_flags::GetServerConfigurableFlag(
+ "aconfig_flags.aconfig_test",
+ "com.android.aconfig.test.enabled_ro_exported",
+ "false") == "true";
+ }
+ return cache_[2];
+ }
+
+ private:
+ std::vector<int8_t> cache_ = std::vector<int8_t>(3, -1);
+ };
+
+ std::unique_ptr<flag_provider_interface> provider_ =
+ std::make_unique<flag_provider>();
+}
+
+bool com_android_aconfig_test_disabled_rw_exported() {
+ return com::android::aconfig::test::disabled_rw_exported();
+}
+
+bool com_android_aconfig_test_enabled_fixed_ro_exported() {
+ return com::android::aconfig::test::enabled_fixed_ro_exported();
+}
+
+bool com_android_aconfig_test_enabled_ro_exported() {
+ return com::android::aconfig::test::enabled_ro_exported();
+}
+
+
+"#;
+
const READ_ONLY_EXPORTED_PROD_HEADER_EXPECTED: &str = r#"
#pragma once
@@ -854,12 +1027,11 @@
expected_header: &str,
expected_src: &str,
) {
- let generated = generate_cpp_code(
- crate::test::TEST_PACKAGE,
- parsed_flags.parsed_flag.into_iter(),
- mode,
- )
- .unwrap();
+ let modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode);
+ let generated =
+ generate_cpp_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+ .unwrap();
let mut generated_files_map = HashMap::new();
for file in generated {
generated_files_map.insert(
@@ -912,6 +1084,17 @@
}
#[test]
+ fn test_generate_cpp_code_for_exported() {
+ let parsed_flags = crate::test::parse_test_flags();
+ test_generate_cpp_code(
+ parsed_flags,
+ CodegenMode::Exported,
+ EXPORTED_EXPORTED_HEADER_EXPECTED,
+ EXPORTED_SOURCE_FILE_EXPECTED,
+ );
+ }
+
+ #[test]
fn test_generate_cpp_code_for_read_only_prod() {
let parsed_flags = crate::test::parse_read_only_test_flags();
test_generate_cpp_code(
diff --git a/tools/aconfig/src/codegen/java.rs b/tools/aconfig/src/codegen/java.rs
index 1d5dabf..6a7d7c1 100644
--- a/tools/aconfig/src/codegen/java.rs
+++ b/tools/aconfig/src/codegen/java.rs
@@ -21,7 +21,8 @@
use tinytemplate::TinyTemplate;
use crate::codegen;
-use crate::commands::{CodegenMode, OutputFile};
+use crate::codegen::CodegenMode;
+use crate::commands::OutputFile;
use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
pub fn generate_java_code<I>(
@@ -202,6 +203,9 @@
boolean enabledFixedRo();
@com.android.aconfig.annotations.AssumeTrueForR8
@UnsupportedAppUsage
+ boolean enabledFixedRoExported();
+ @com.android.aconfig.annotations.AssumeTrueForR8
+ @UnsupportedAppUsage
boolean enabledRo();
@com.android.aconfig.annotations.AssumeTrueForR8
@UnsupportedAppUsage
@@ -228,6 +232,8 @@
/** @hide */
public static final String FLAG_ENABLED_FIXED_RO = "com.android.aconfig.test.enabled_fixed_ro";
/** @hide */
+ public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported";
+ /** @hide */
public static final String FLAG_ENABLED_RO = "com.android.aconfig.test.enabled_ro";
/** @hide */
public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported";
@@ -258,6 +264,11 @@
}
@com.android.aconfig.annotations.AssumeTrueForR8
@UnsupportedAppUsage
+ public static boolean enabledFixedRoExported() {
+ return FEATURE_FLAGS.enabledFixedRoExported();
+ }
+ @com.android.aconfig.annotations.AssumeTrueForR8
+ @UnsupportedAppUsage
public static boolean enabledRo() {
return FEATURE_FLAGS.enabledRo();
}
@@ -310,6 +321,11 @@
}
@Override
@UnsupportedAppUsage
+ public boolean enabledFixedRoExported() {
+ return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED);
+ }
+ @Override
+ @UnsupportedAppUsage
public boolean enabledRo() {
return getValue(Flags.FLAG_ENABLED_RO);
}
@@ -348,6 +364,7 @@
Map.entry(Flags.FLAG_DISABLED_RW_EXPORTED, false),
Map.entry(Flags.FLAG_DISABLED_RW_IN_OTHER_NAMESPACE, false),
Map.entry(Flags.FLAG_ENABLED_FIXED_RO, false),
+ Map.entry(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, false),
Map.entry(Flags.FLAG_ENABLED_RO, false),
Map.entry(Flags.FLAG_ENABLED_RO_EXPORTED, false),
Map.entry(Flags.FLAG_ENABLED_RW, false)
@@ -463,6 +480,11 @@
}
@Override
@UnsupportedAppUsage
+ public boolean enabledFixedRoExported() {
+ return true;
+ }
+ @Override
+ @UnsupportedAppUsage
public boolean enabledRo() {
return true;
}
@@ -528,6 +550,8 @@
/** @hide */
public static final String FLAG_DISABLED_RW_EXPORTED = "com.android.aconfig.test.disabled_rw_exported";
/** @hide */
+ public static final String FLAG_ENABLED_FIXED_RO_EXPORTED = "com.android.aconfig.test.enabled_fixed_ro_exported";
+ /** @hide */
public static final String FLAG_ENABLED_RO_EXPORTED = "com.android.aconfig.test.enabled_ro_exported";
@UnsupportedAppUsage
@@ -535,6 +559,10 @@
return FEATURE_FLAGS.disabledRwExported();
}
@UnsupportedAppUsage
+ public static boolean enabledFixedRoExported() {
+ return FEATURE_FLAGS.enabledFixedRoExported();
+ }
+ @UnsupportedAppUsage
public static boolean enabledRoExported() {
return FEATURE_FLAGS.enabledRoExported();
}
@@ -551,6 +579,8 @@
@UnsupportedAppUsage
boolean disabledRwExported();
@UnsupportedAppUsage
+ boolean enabledFixedRoExported();
+ @UnsupportedAppUsage
boolean enabledRoExported();
}
"#;
@@ -566,6 +596,7 @@
private static boolean aconfig_test_is_cached = false;
private static boolean other_namespace_is_cached = false;
private static boolean disabledRwExported = false;
+ private static boolean enabledFixedRoExported = false;
private static boolean enabledRoExported = false;
@@ -574,6 +605,8 @@
Properties properties = DeviceConfig.getProperties("aconfig_test");
disabledRwExported =
properties.getBoolean("com.android.aconfig.test.disabled_rw_exported", false);
+ enabledFixedRoExported =
+ properties.getBoolean("com.android.aconfig.test.enabled_fixed_ro_exported", false);
enabledRoExported =
properties.getBoolean("com.android.aconfig.test.enabled_ro_exported", false);
} catch (NullPointerException e) {
@@ -616,6 +649,15 @@
@Override
@UnsupportedAppUsage
+ public boolean enabledFixedRoExported() {
+ if (!aconfig_test_is_cached) {
+ load_overrides_aconfig_test();
+ }
+ return enabledFixedRoExported;
+ }
+
+ @Override
+ @UnsupportedAppUsage
public boolean enabledRoExported() {
if (!aconfig_test_is_cached) {
load_overrides_aconfig_test();
@@ -642,6 +684,11 @@
}
@Override
@UnsupportedAppUsage
+ public boolean enabledFixedRoExported() {
+ return getValue(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED);
+ }
+ @Override
+ @UnsupportedAppUsage
public boolean enabledRoExported() {
return getValue(Flags.FLAG_ENABLED_RO_EXPORTED);
}
@@ -666,6 +713,7 @@
private Map<String, Boolean> mFlagMap = new HashMap<>(
Map.ofEntries(
Map.entry(Flags.FLAG_DISABLED_RW_EXPORTED, false),
+ Map.entry(Flags.FLAG_ENABLED_FIXED_RO_EXPORTED, false),
Map.entry(Flags.FLAG_ENABLED_RO_EXPORTED, false)
)
);
@@ -759,6 +807,12 @@
}
@Override
@UnsupportedAppUsage
+ public boolean enabledFixedRoExported() {
+ throw new UnsupportedOperationException(
+ "Method is not implemented.");
+ }
+ @Override
+ @UnsupportedAppUsage
public boolean enabledRo() {
throw new UnsupportedOperationException(
"Method is not implemented.");
diff --git a/tools/aconfig/src/codegen/mod.rs b/tools/aconfig/src/codegen/mod.rs
index abc27c6..fc61b7b 100644
--- a/tools/aconfig/src/codegen/mod.rs
+++ b/tools/aconfig/src/codegen/mod.rs
@@ -19,6 +19,7 @@
pub mod rust;
use anyhow::{ensure, Result};
+use clap::ValueEnum;
pub fn is_valid_name_ident(s: &str) -> bool {
// Identifiers must match [a-z][a-z0-9_]*, except consecutive underscores are not allowed
@@ -52,6 +53,13 @@
Ok(format!("{}.{}", package, flag_name))
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
+pub enum CodegenMode {
+ Production,
+ Test,
+ Exported,
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/tools/aconfig/src/codegen/rust.rs b/tools/aconfig/src/codegen/rust.rs
index 7aafddb..6cf0b32 100644
--- a/tools/aconfig/src/codegen/rust.rs
+++ b/tools/aconfig/src/codegen/rust.rs
@@ -19,7 +19,8 @@
use tinytemplate::TinyTemplate;
use crate::codegen;
-use crate::commands::{CodegenMode, OutputFile};
+use crate::codegen::CodegenMode;
+use crate::commands::OutputFile;
use crate::protos::{ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag};
pub fn generate_rust_code<I>(
@@ -45,9 +46,7 @@
match codegen_mode {
CodegenMode::Production => include_str!("../../templates/rust_prod.template"),
CodegenMode::Test => include_str!("../../templates/rust_test.template"),
- CodegenMode::Exported => {
- todo!("exported mode not yet supported for rust, see b/313894653.")
- }
+ CodegenMode::Exported => include_str!("../../templates/rust_exported.template"),
},
)?;
let contents = template.render("rust_code_gen", &context)?;
@@ -153,6 +152,11 @@
true
}
+ /// query flag enabled_fixed_ro_exported
+ pub fn enabled_fixed_ro_exported(&self) -> bool {
+ true
+ }
+
/// query flag enabled_ro
pub fn enabled_ro(&self) -> bool {
true
@@ -202,6 +206,12 @@
true
}
+/// query flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn enabled_fixed_ro_exported() -> bool {
+ true
+}
+
/// query flag enabled_ro
#[inline(always)]
pub fn enabled_ro() -> bool {
@@ -302,6 +312,18 @@
self.overrides.insert("enabled_fixed_ro", val);
}
+ /// query flag enabled_fixed_ro_exported
+ pub fn enabled_fixed_ro_exported(&self) -> bool {
+ self.overrides.get("enabled_fixed_ro_exported").copied().unwrap_or(
+ true
+ )
+ }
+
+ /// set flag enabled_fixed_ro_exported
+ pub fn set_enabled_fixed_ro_exported(&mut self, val: bool) {
+ self.overrides.insert("enabled_fixed_ro_exported", val);
+ }
+
/// query flag enabled_ro
pub fn enabled_ro(&self) -> bool {
self.overrides.get("enabled_ro").copied().unwrap_or(
@@ -412,6 +434,18 @@
PROVIDER.lock().unwrap().set_enabled_fixed_ro(val);
}
+/// query flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn enabled_fixed_ro_exported() -> bool {
+ PROVIDER.lock().unwrap().enabled_fixed_ro_exported()
+}
+
+/// set flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn set_enabled_fixed_ro_exported(val: bool) {
+ PROVIDER.lock().unwrap().set_enabled_fixed_ro_exported(val);
+}
+
/// query flag enabled_ro
#[inline(always)]
pub fn enabled_ro() -> bool {
@@ -454,14 +488,79 @@
}
"#;
+ const EXPORTED_EXPECTED: &str = r#"
+//! codegenerated rust flag lib
+
+/// flag provider
+pub struct FlagProvider;
+
+lazy_static::lazy_static! {
+ /// flag value cache for disabled_rw_exported
+ static ref CACHED_disabled_rw_exported: bool = flags_rust::GetServerConfigurableFlag(
+ "aconfig_flags.aconfig_test",
+ "com.android.aconfig.test.disabled_rw_exported",
+ "false") == "true";
+
+ /// flag value cache for enabled_fixed_ro_exported
+ static ref CACHED_enabled_fixed_ro_exported: bool = flags_rust::GetServerConfigurableFlag(
+ "aconfig_flags.aconfig_test",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
+ "false") == "true";
+
+ /// flag value cache for enabled_ro_exported
+ static ref CACHED_enabled_ro_exported: bool = flags_rust::GetServerConfigurableFlag(
+ "aconfig_flags.aconfig_test",
+ "com.android.aconfig.test.enabled_ro_exported",
+ "false") == "true";
+
+}
+
+impl FlagProvider {
+ /// query flag disabled_rw_exported
+ pub fn disabled_rw_exported(&self) -> bool {
+ *CACHED_disabled_rw_exported
+ }
+
+ /// query flag enabled_fixed_ro_exported
+ pub fn enabled_fixed_ro_exported(&self) -> bool {
+ *CACHED_enabled_fixed_ro_exported
+ }
+
+ /// query flag enabled_ro_exported
+ pub fn enabled_ro_exported(&self) -> bool {
+ *CACHED_enabled_ro_exported
+ }
+}
+
+/// flag provider
+pub static PROVIDER: FlagProvider = FlagProvider;
+
+/// query flag disabled_rw_exported
+#[inline(always)]
+pub fn disabled_rw_exported() -> bool {
+ PROVIDER.disabled_rw_exported()
+}
+
+/// query flag enabled_fixed_ro_exported
+#[inline(always)]
+pub fn enabled_fixed_ro_exported() -> bool {
+ PROVIDER.enabled_fixed_ro_exported()
+}
+
+/// query flag enabled_ro_exported
+#[inline(always)]
+pub fn enabled_ro_exported() -> bool {
+ PROVIDER.enabled_ro_exported()
+}
+"#;
+
fn test_generate_rust_code(mode: CodegenMode) {
let parsed_flags = crate::test::parse_test_flags();
- let generated = generate_rust_code(
- crate::test::TEST_PACKAGE,
- parsed_flags.parsed_flag.into_iter(),
- mode,
- )
- .unwrap();
+ let modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode);
+ let generated =
+ generate_rust_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+ .unwrap();
assert_eq!("src/lib.rs", format!("{}", generated.path.display()));
assert_eq!(
None,
@@ -469,8 +568,7 @@
match mode {
CodegenMode::Production => PROD_EXPECTED,
CodegenMode::Test => TEST_EXPECTED,
- CodegenMode::Exported =>
- todo!("exported mode not yet supported for rust, see b/313894653."),
+ CodegenMode::Exported => EXPORTED_EXPECTED,
},
&String::from_utf8(generated.contents).unwrap()
)
@@ -486,4 +584,9 @@
fn test_generate_rust_code_for_test() {
test_generate_rust_code(CodegenMode::Test);
}
+
+ #[test]
+ fn test_generate_rust_code_for_exported() {
+ test_generate_rust_code(CodegenMode::Exported);
+ }
}
diff --git a/tools/aconfig/src/commands.rs b/tools/aconfig/src/commands.rs
index 87905fd..23667bb 100644
--- a/tools/aconfig/src/commands.rs
+++ b/tools/aconfig/src/commands.rs
@@ -15,7 +15,6 @@
*/
use anyhow::{bail, ensure, Context, Result};
-use clap::ValueEnum;
use protobuf::Message;
use std::io::Read;
use std::path::PathBuf;
@@ -23,6 +22,7 @@
use crate::codegen::cpp::generate_cpp_code;
use crate::codegen::java::generate_java_code;
use crate::codegen::rust::generate_rust_code;
+use crate::codegen::CodegenMode;
use crate::dump::{DumpFormat, DumpPredicate};
use crate::protos::{
ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag,
@@ -188,13 +188,6 @@
Ok(output)
}
-#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
-pub enum CodegenMode {
- Production,
- Test,
- Exported,
-}
-
pub fn create_java_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<Vec<OutputFile>> {
let parsed_flags = input.try_parse_flags()?;
let filtered_parsed_flags = filter_parsed_flags(parsed_flags, codegen_mode);
@@ -207,22 +200,22 @@
pub fn create_cpp_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<Vec<OutputFile>> {
let parsed_flags = input.try_parse_flags()?;
- let filtered_parsed_flags = filter_parsed_flags(parsed_flags, codegen_mode);
- let Some(package) = find_unique_package(&filtered_parsed_flags) else {
+ let modified_parsed_flags = modify_parsed_flags_based_on_mode(parsed_flags, codegen_mode);
+ let Some(package) = find_unique_package(&modified_parsed_flags) else {
bail!("no parsed flags, or the parsed flags use different packages");
};
let package = package.to_string();
- generate_cpp_code(&package, filtered_parsed_flags.into_iter(), codegen_mode)
+ generate_cpp_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
}
pub fn create_rust_lib(mut input: Input, codegen_mode: CodegenMode) -> Result<OutputFile> {
let parsed_flags = input.try_parse_flags()?;
- let filtered_parsed_flags = filter_parsed_flags(parsed_flags, codegen_mode);
- let Some(package) = find_unique_package(&filtered_parsed_flags) else {
+ let modified_parsed_flags = modify_parsed_flags_based_on_mode(parsed_flags, codegen_mode);
+ let Some(package) = find_unique_package(&modified_parsed_flags) else {
bail!("no parsed flags, or the parsed flags use different packages");
};
let package = package.to_string();
- generate_rust_code(&package, filtered_parsed_flags.into_iter(), codegen_mode)
+ generate_rust_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
}
pub fn create_storage(caches: Vec<Input>, container: &str) -> Result<Vec<OutputFile>> {
@@ -335,6 +328,30 @@
}
}
+pub fn modify_parsed_flags_based_on_mode(
+ parsed_flags: ProtoParsedFlags,
+ codegen_mode: CodegenMode,
+) -> Vec<ProtoParsedFlag> {
+ fn exported_mode_flag_modifier(mut parsed_flag: ProtoParsedFlag) -> ProtoParsedFlag {
+ parsed_flag.set_state(ProtoFlagState::DISABLED);
+ parsed_flag.set_permission(ProtoFlagPermission::READ_WRITE);
+ parsed_flag.set_is_fixed_read_only(false);
+ parsed_flag
+ }
+
+ match codegen_mode {
+ CodegenMode::Exported => parsed_flags
+ .parsed_flag
+ .into_iter()
+ .filter(|pf| pf.is_exported())
+ .map(exported_mode_flag_modifier)
+ .collect(),
+ CodegenMode::Production | CodegenMode::Test => {
+ parsed_flags.parsed_flag.into_iter().collect()
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -366,9 +383,9 @@
assert_eq!(ProtoFlagState::ENABLED, enabled_ro.trace[2].state());
assert_eq!(ProtoFlagPermission::READ_ONLY, enabled_ro.trace[2].permission());
- assert_eq!(8, parsed_flags.parsed_flag.len());
+ assert_eq!(9, parsed_flags.parsed_flag.len());
for pf in parsed_flags.parsed_flag.iter() {
- if pf.name() == "enabled_fixed_ro" {
+ if pf.name().starts_with("enabled_fixed_ro") {
continue;
}
let first = pf.trace.first().unwrap();
@@ -597,7 +614,13 @@
let bytes =
dump_parsed_flags(vec![input, input2], DumpFormat::Textproto, &[], true).unwrap();
let text = std::str::from_utf8(&bytes).unwrap();
- assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
+ assert_eq!(
+ None,
+ crate::test::first_significant_code_diff(
+ crate::test::TEST_FLAGS_TEXTPROTO.trim(),
+ text.trim()
+ )
+ );
}
#[test]
@@ -607,14 +630,14 @@
let filtered_parsed_flags =
filter_parsed_flags(parsed_flags.clone(), CodegenMode::Exported);
- assert_eq!(2, filtered_parsed_flags.len());
+ assert_eq!(3, filtered_parsed_flags.len());
let filtered_parsed_flags =
filter_parsed_flags(parsed_flags.clone(), CodegenMode::Production);
- assert_eq!(8, filtered_parsed_flags.len());
+ assert_eq!(9, filtered_parsed_flags.len());
let filtered_parsed_flags = filter_parsed_flags(parsed_flags.clone(), CodegenMode::Test);
- assert_eq!(8, filtered_parsed_flags.len());
+ assert_eq!(9, filtered_parsed_flags.len());
}
fn parse_test_flags_as_input() -> Input {
@@ -624,4 +647,28 @@
let reader = Box::new(cursor);
Input { source: "test.data".to_string(), reader }
}
+
+ #[test]
+ fn test_modify_parsed_flags_based_on_mode_prod() {
+ let parsed_flags = crate::test::parse_test_flags();
+ let p_parsed_flags =
+ modify_parsed_flags_based_on_mode(parsed_flags.clone(), CodegenMode::Production);
+ assert_eq!(parsed_flags.parsed_flag.len(), p_parsed_flags.len());
+ for (i, item) in p_parsed_flags.iter().enumerate() {
+ assert!(parsed_flags.parsed_flag[i].eq(item));
+ }
+ }
+
+ #[test]
+ fn test_modify_parsed_flags_based_on_mode_exported() {
+ let parsed_flags = crate::test::parse_test_flags();
+ let p_parsed_flags = modify_parsed_flags_based_on_mode(parsed_flags, CodegenMode::Exported);
+ assert_eq!(3, p_parsed_flags.len());
+ for flag in p_parsed_flags.iter() {
+ assert_eq!(ProtoFlagState::DISABLED, flag.state());
+ assert_eq!(ProtoFlagPermission::READ_WRITE, flag.permission());
+ assert!(!flag.is_fixed_read_only());
+ assert!(flag.is_exported());
+ }
+ }
}
diff --git a/tools/aconfig/src/dump.rs b/tools/aconfig/src/dump.rs
index f2b3687..bcad064 100644
--- a/tools/aconfig/src/dump.rs
+++ b/tools/aconfig/src/dump.rs
@@ -197,6 +197,10 @@
Ok(Box::new(move |flag: &ProtoParsedFlag| flag.container() == expected))
}
// metadata: not supported yet
+ "fully_qualified_name" => {
+ let expected = arg.to_owned();
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.fully_qualified_name() == expected))
+ }
_ => Err(anyhow!(error_msg)),
}
}
@@ -341,6 +345,7 @@
"com.android.aconfig.test.disabled_rw_exported",
"com.android.aconfig.test.disabled_rw_in_other_namespace",
"com.android.aconfig.test.enabled_fixed_ro",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
"com.android.aconfig.test.enabled_ro",
"com.android.aconfig.test.enabled_ro_exported",
"com.android.aconfig.test.enabled_rw",
@@ -360,6 +365,7 @@
"state:ENABLED",
&[
"com.android.aconfig.test.enabled_fixed_ro",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
"com.android.aconfig.test.enabled_ro",
"com.android.aconfig.test.enabled_ro_exported",
"com.android.aconfig.test.enabled_rw",
@@ -370,6 +376,7 @@
&[
"com.android.aconfig.test.disabled_ro",
"com.android.aconfig.test.enabled_fixed_ro",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
"com.android.aconfig.test.enabled_ro",
"com.android.aconfig.test.enabled_ro_exported",
]
@@ -377,12 +384,16 @@
// trace: not supported yet
assert_create_filter_predicate!(
"is_fixed_read_only:true",
- &["com.android.aconfig.test.enabled_fixed_ro"]
+ &[
+ "com.android.aconfig.test.enabled_fixed_ro",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
+ ]
);
assert_create_filter_predicate!(
"is_exported:true",
&[
"com.android.aconfig.test.disabled_rw_exported",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
"com.android.aconfig.test.enabled_ro_exported",
]
);
@@ -394,6 +405,7 @@
"com.android.aconfig.test.disabled_rw_exported",
"com.android.aconfig.test.disabled_rw_in_other_namespace",
"com.android.aconfig.test.enabled_fixed_ro",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
"com.android.aconfig.test.enabled_ro",
"com.android.aconfig.test.enabled_ro_exported",
"com.android.aconfig.test.enabled_rw",
@@ -401,11 +413,18 @@
);
// metadata: not supported yet
+ // synthesized fields
+ assert_create_filter_predicate!(
+ "fully_qualified_name:com.android.aconfig.test.disabled_rw",
+ &["com.android.aconfig.test.disabled_rw"]
+ );
+
// multiple sub filters
assert_create_filter_predicate!(
"permission:READ_ONLY+state:ENABLED",
&[
"com.android.aconfig.test.enabled_fixed_ro",
+ "com.android.aconfig.test.enabled_fixed_ro_exported",
"com.android.aconfig.test.enabled_ro",
"com.android.aconfig.test.enabled_ro_exported",
]
diff --git a/tools/aconfig/src/main.rs b/tools/aconfig/src/main.rs
index 437d124..59f3677 100644
--- a/tools/aconfig/src/main.rs
+++ b/tools/aconfig/src/main.rs
@@ -30,12 +30,13 @@
mod protos;
mod storage;
+use codegen::CodegenMode;
use dump::DumpFormat;
#[cfg(test)]
mod test;
-use commands::{CodegenMode, Input, OutputFile};
+use commands::{Input, OutputFile};
const HELP_DUMP_FILTER: &str = r#"
Limit which flags to output. If multiple --filter arguments are provided, the output will be
@@ -69,7 +70,7 @@
.arg(
Arg::new("mode")
.long("mode")
- .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+ .value_parser(EnumValueParser::<CodegenMode>::new())
.default_value("production"),
),
)
@@ -80,7 +81,7 @@
.arg(
Arg::new("mode")
.long("mode")
- .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+ .value_parser(EnumValueParser::<CodegenMode>::new())
.default_value("production"),
),
)
@@ -91,7 +92,7 @@
.arg(
Arg::new("mode")
.long("mode")
- .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+ .value_parser(EnumValueParser::<CodegenMode>::new())
.default_value("production"),
),
)
diff --git a/tools/aconfig/src/storage/mod.rs b/tools/aconfig/src/storage/mod.rs
index 90e05f5..f81fb5c 100644
--- a/tools/aconfig/src/storage/mod.rs
+++ b/tools/aconfig/src/storage/mod.rs
@@ -14,11 +14,41 @@
* limitations under the License.
*/
-use anyhow::Result;
-use std::collections::{HashMap, HashSet};
+pub mod package_table;
+
+use anyhow::{anyhow, Result};
+use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
+use std::hash::{Hash, Hasher};
+use std::path::PathBuf;
use crate::commands::OutputFile;
use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
+use crate::storage::package_table::PackageTable;
+
+pub const FILE_VERSION: u32 = 1;
+
+pub const HASH_PRIMES: [u32; 29] = [
+ 7, 13, 29, 53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241,
+ 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611,
+ 402653189, 805306457, 1610612741,
+];
+
+/// Get the right hash table size given number of entries in the table. Use a
+/// load factor of 0.5 for performance.
+pub fn get_table_size(entries: u32) -> Result<u32> {
+ HASH_PRIMES
+ .iter()
+ .find(|&&num| num >= 2 * entries)
+ .copied()
+ .ok_or(anyhow!("Number of packages is too large"))
+}
+
+/// Get the corresponding bucket index given the key and number of buckets
+pub fn get_bucket_index<T: Hash>(val: &T, num_buckets: u32) -> u32 {
+ let mut s = DefaultHasher::new();
+ val.hash(&mut s);
+ (s.finish() % num_buckets as u64) as u32
+}
pub struct FlagPackage<'a> {
pub package_name: &'a str,
@@ -52,7 +82,7 @@
{
// group flags by package
let mut packages: Vec<FlagPackage<'a>> = Vec::new();
- let mut package_index: HashMap<&'a str, usize> = HashMap::new();
+ let mut package_index: HashMap<&str, usize> = HashMap::new();
for parsed_flags in parsed_flags_vec_iter {
for parsed_flag in parsed_flags.parsed_flag.iter() {
let index = *(package_index.entry(parsed_flag.package()).or_insert(packages.len()));
@@ -76,14 +106,21 @@
}
pub fn generate_storage_files<'a, I>(
- _containser: &str,
+ container: &str,
parsed_flags_vec_iter: I,
) -> Result<Vec<OutputFile>>
where
I: Iterator<Item = &'a ProtoParsedFlags>,
{
- let _packages = group_flags_by_package(parsed_flags_vec_iter);
- Ok(vec![])
+ let packages = group_flags_by_package(parsed_flags_vec_iter);
+
+ // create and serialize package map
+ let package_table = PackageTable::new(container, &packages)?;
+ let package_table_file_path = PathBuf::from("package.map");
+ let package_table_file =
+ OutputFile { contents: package_table.as_bytes(), path: package_table_file_path };
+
+ Ok(vec![package_table_file])
}
#[cfg(test)]
@@ -91,6 +128,21 @@
use super::*;
use crate::Input;
+ /// Read and parse bytes as u32
+ pub fn read_u32_from_bytes(buf: &[u8], head: &mut usize) -> Result<u32> {
+ let val = u32::from_le_bytes(buf[*head..*head + 4].try_into()?);
+ *head += 4;
+ Ok(val)
+ }
+
+ /// Read and parse bytes as string
+ pub fn read_str_from_bytes(buf: &[u8], head: &mut usize) -> Result<String> {
+ let num_bytes = read_u32_from_bytes(buf, head)? as usize;
+ let val = String::from_utf8(buf[*head..*head + num_bytes].to_vec())?;
+ *head += num_bytes;
+ Ok(val)
+ }
+
pub fn parse_all_test_flags() -> Vec<ProtoParsedFlags> {
let aconfig_files = [
(
diff --git a/tools/aconfig/src/storage/package_table.rs b/tools/aconfig/src/storage/package_table.rs
new file mode 100644
index 0000000..78102a5
--- /dev/null
+++ b/tools/aconfig/src/storage/package_table.rs
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+use crate::storage::{self, FlagPackage};
+use anyhow::Result;
+
+#[derive(PartialEq, Debug)]
+pub struct PackageTableHeader {
+ pub version: u32,
+ pub container: String,
+ pub file_size: u32,
+ pub num_packages: u32,
+ pub bucket_offset: u32,
+ pub node_offset: u32,
+}
+
+impl PackageTableHeader {
+ fn new(container: &str, num_packages: u32) -> Self {
+ Self {
+ version: storage::FILE_VERSION,
+ container: String::from(container),
+ file_size: 0,
+ num_packages,
+ bucket_offset: 0,
+ node_offset: 0,
+ }
+ }
+
+ fn as_bytes(&self) -> Vec<u8> {
+ let mut result = Vec::new();
+ result.extend_from_slice(&self.version.to_le_bytes());
+ let container_bytes = self.container.as_bytes();
+ result.extend_from_slice(&(container_bytes.len() as u32).to_le_bytes());
+ result.extend_from_slice(container_bytes);
+ result.extend_from_slice(&self.file_size.to_le_bytes());
+ result.extend_from_slice(&self.num_packages.to_le_bytes());
+ result.extend_from_slice(&self.bucket_offset.to_le_bytes());
+ result.extend_from_slice(&self.node_offset.to_le_bytes());
+ result
+ }
+}
+
+#[derive(PartialEq, Debug)]
+pub struct PackageTableNode {
+ pub package_name: String,
+ pub package_id: u32,
+ pub boolean_offset: u32,
+ pub next_offset: Option<u32>,
+ pub bucket_index: u32,
+}
+
+impl PackageTableNode {
+ fn new(package: &FlagPackage, num_buckets: u32) -> Self {
+ let bucket_index =
+ storage::get_bucket_index(&package.package_name.to_string(), num_buckets);
+ Self {
+ package_name: String::from(package.package_name),
+ package_id: package.package_id,
+ boolean_offset: package.boolean_offset,
+ next_offset: None,
+ bucket_index,
+ }
+ }
+
+ fn as_bytes(&self) -> Vec<u8> {
+ let mut result = Vec::new();
+ let name_bytes = self.package_name.as_bytes();
+ result.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
+ result.extend_from_slice(name_bytes);
+ result.extend_from_slice(&self.package_id.to_le_bytes());
+ result.extend_from_slice(&self.boolean_offset.to_le_bytes());
+ result.extend_from_slice(&self.next_offset.unwrap_or(0).to_le_bytes());
+ result
+ }
+}
+
+pub struct PackageTable {
+ pub header: PackageTableHeader,
+ pub buckets: Vec<Option<u32>>,
+ pub nodes: Vec<PackageTableNode>,
+}
+
+impl PackageTable {
+ pub fn new(container: &str, packages: &[FlagPackage]) -> Result<Self> {
+ // create table
+ let num_packages = packages.len() as u32;
+ let num_buckets = storage::get_table_size(num_packages)?;
+ let mut table = Self {
+ header: PackageTableHeader::new(container, num_packages),
+ buckets: vec![None; num_buckets as usize],
+ nodes: packages.iter().map(|pkg| PackageTableNode::new(pkg, num_buckets)).collect(),
+ };
+
+ // sort nodes by bucket index for efficiency
+ table.nodes.sort_by(|a, b| a.bucket_index.cmp(&b.bucket_index));
+
+ // fill all node offset
+ let mut offset = 0;
+ for i in 0..table.nodes.len() {
+ let node_bucket_idx = table.nodes[i].bucket_index;
+ let next_node_bucket_idx = if i + 1 < table.nodes.len() {
+ Some(table.nodes[i + 1].bucket_index)
+ } else {
+ None
+ };
+
+ if table.buckets[node_bucket_idx as usize].is_none() {
+ table.buckets[node_bucket_idx as usize] = Some(offset);
+ }
+ offset += table.nodes[i].as_bytes().len() as u32;
+
+ if let Some(index) = next_node_bucket_idx {
+ if index == node_bucket_idx {
+ table.nodes[i].next_offset = Some(offset);
+ }
+ }
+ }
+
+ // fill table region offset
+ table.header.bucket_offset = table.header.as_bytes().len() as u32;
+ table.header.node_offset = table.header.bucket_offset + num_buckets * 4;
+ table.header.file_size = table.header.node_offset
+ + table.nodes.iter().map(|x| x.as_bytes().len()).sum::<usize>() as u32;
+
+ Ok(table)
+ }
+
+ pub fn as_bytes(&self) -> Vec<u8> {
+ [
+ self.header.as_bytes(),
+ self.buckets.iter().map(|v| v.unwrap_or(0).to_le_bytes()).collect::<Vec<_>>().concat(),
+ self.nodes.iter().map(|v| v.as_bytes()).collect::<Vec<_>>().concat(),
+ ]
+ .concat()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::storage::{
+ group_flags_by_package, tests::parse_all_test_flags, tests::read_str_from_bytes,
+ tests::read_u32_from_bytes,
+ };
+
+ impl PackageTableHeader {
+ // test only method to deserialize back into the header struct
+ fn from_bytes(bytes: &[u8]) -> Result<Self> {
+ let mut head = 0;
+ Ok(Self {
+ version: read_u32_from_bytes(bytes, &mut head)?,
+ container: read_str_from_bytes(bytes, &mut head)?,
+ file_size: read_u32_from_bytes(bytes, &mut head)?,
+ num_packages: read_u32_from_bytes(bytes, &mut head)?,
+ bucket_offset: read_u32_from_bytes(bytes, &mut head)?,
+ node_offset: read_u32_from_bytes(bytes, &mut head)?,
+ })
+ }
+ }
+
+ impl PackageTableNode {
+ // test only method to deserialize back into the node struct
+ fn from_bytes(bytes: &[u8], num_buckets: u32) -> Result<Self> {
+ let mut head = 0;
+ let mut node = Self {
+ package_name: read_str_from_bytes(bytes, &mut head)?,
+ package_id: read_u32_from_bytes(bytes, &mut head)?,
+ boolean_offset: read_u32_from_bytes(bytes, &mut head)?,
+ next_offset: match read_u32_from_bytes(bytes, &mut head)? {
+ 0 => None,
+ val => Some(val),
+ },
+ bucket_index: 0,
+ };
+ node.bucket_index = storage::get_bucket_index(&node.package_name, num_buckets);
+ Ok(node)
+ }
+ }
+
+ pub fn create_test_package_table() -> Result<PackageTable> {
+ let caches = parse_all_test_flags();
+ let packages = group_flags_by_package(caches.iter());
+ PackageTable::new("system", &packages)
+ }
+
+ #[test]
+ // this test point locks down the table creation and each field
+ fn test_table_contents() {
+ let package_table = create_test_package_table();
+ assert!(package_table.is_ok());
+
+ let header: &PackageTableHeader = &package_table.as_ref().unwrap().header;
+ let expected_header = PackageTableHeader {
+ version: storage::FILE_VERSION,
+ container: String::from("system"),
+ file_size: 158,
+ num_packages: 2,
+ bucket_offset: 30,
+ node_offset: 58,
+ };
+ assert_eq!(header, &expected_header);
+
+ let buckets: &Vec<Option<u32>> = &package_table.as_ref().unwrap().buckets;
+ let expected: Vec<Option<u32>> = vec![Some(0), None, None, Some(50), None, None, None];
+ assert_eq!(buckets, &expected);
+
+ let nodes: &Vec<PackageTableNode> = &package_table.as_ref().unwrap().nodes;
+ assert_eq!(nodes.len(), 2);
+ let first_node_expected = PackageTableNode {
+ package_name: String::from("com.android.aconfig.storage.test_2"),
+ package_id: 1,
+ boolean_offset: 10,
+ next_offset: None,
+ bucket_index: 0,
+ };
+ assert_eq!(nodes[0], first_node_expected);
+ let second_node_expected = PackageTableNode {
+ package_name: String::from("com.android.aconfig.storage.test_1"),
+ package_id: 0,
+ boolean_offset: 0,
+ next_offset: None,
+ bucket_index: 3,
+ };
+ assert_eq!(nodes[1], second_node_expected);
+ }
+
+ #[test]
+ // this test point locks down the table serialization
+ fn test_serialization() {
+ let package_table = create_test_package_table();
+ assert!(package_table.is_ok());
+
+ let header: &PackageTableHeader = &package_table.as_ref().unwrap().header;
+ let reinterpreted_header = PackageTableHeader::from_bytes(&header.as_bytes());
+ assert!(reinterpreted_header.is_ok());
+ assert_eq!(header, &reinterpreted_header.unwrap());
+
+ let nodes: &Vec<PackageTableNode> = &package_table.as_ref().unwrap().nodes;
+ let num_buckets = storage::get_table_size(header.num_packages).unwrap();
+ for node in nodes.iter() {
+ let reinterpreted_node = PackageTableNode::from_bytes(&node.as_bytes(), num_buckets);
+ assert!(reinterpreted_node.is_ok());
+ assert_eq!(node, &reinterpreted_node.unwrap());
+ }
+ }
+}
diff --git a/tools/aconfig/src/test.rs b/tools/aconfig/src/test.rs
index 309cb28..cbb95b8 100644
--- a/tools/aconfig/src/test.rs
+++ b/tools/aconfig/src/test.rs
@@ -145,6 +145,31 @@
}
parsed_flag {
package: "com.android.aconfig.test"
+ name: "enabled_fixed_ro_exported"
+ namespace: "aconfig_test"
+ description: "This flag is fixed ENABLED + READ_ONLY and exported"
+ bug: "111"
+ state: ENABLED
+ permission: READ_ONLY
+ trace {
+ source: "tests/test.aconfig"
+ state: DISABLED
+ permission: READ_ONLY
+ }
+ trace {
+ source: "tests/first.values"
+ state: ENABLED
+ permission: READ_ONLY
+ }
+ is_fixed_read_only: true
+ is_exported: true
+ container: "system"
+ metadata {
+ purpose: PURPOSE_UNSPECIFIED
+ }
+}
+parsed_flag {
+ package: "com.android.aconfig.test"
name: "enabled_ro"
namespace: "aconfig_test"
description: "This flag is ENABLED + READ_ONLY"
diff --git a/tools/aconfig/templates/cpp_exported_header.template b/tools/aconfig/templates/cpp_exported_header.template
index 377295d..8db9ec4 100644
--- a/tools/aconfig/templates/cpp_exported_header.template
+++ b/tools/aconfig/templates/cpp_exported_header.template
@@ -1,6 +1,6 @@
#pragma once
-{{ if not for_test- }}
+{{ if not is_test_mode- }}
{{ if has_fixed_read_only- }}
#ifndef {package_macro}
#define {package_macro}(FLAG) {package_macro}_##FLAG
@@ -29,12 +29,12 @@
{{ for item in class_elements}}
virtual bool {item.flag_name}() = 0;
- {{ if for_test- }}
+ {{ if is_test_mode }}
virtual void {item.flag_name}(bool val) = 0;
{{ -endif }}
{{ -endfor }}
- {{ if for_test }}
+ {{ if is_test_mode }}
virtual void reset_flags() \{}
{{ -endif }}
};
@@ -43,9 +43,10 @@
{{ for item in class_elements}}
inline bool {item.flag_name}() \{
- {{ if for_test- }}
+ {{ if is_test_mode }}
return provider_->{item.flag_name}();
{{ -else- }}
+ {{ if is_prod_mode- }}
{{ if item.readwrite- }}
return provider_->{item.flag_name}();
{{ -else- }}
@@ -55,17 +56,22 @@
return {item.default_value};
{{ -endif }}
{{ -endif }}
+ {{ -else- }}
+ {{ if is_exported_mode- }}
+ return provider_->{item.flag_name}();
+ {{ -endif }}
+ {{ -endif }}
{{ -endif }}
}
-{{ if for_test- }}
+{{ if is_test_mode }}
inline void {item.flag_name}(bool val) \{
provider_->{item.flag_name}(val);
}
{{ -endif }}
{{ -endfor }}
-{{ if for_test- }}
+{{ if is_test_mode }}
inline void reset_flags() \{
return provider_->reset_flags();
}
@@ -79,12 +85,12 @@
{{ for item in class_elements }}
bool {header}_{item.flag_name}();
-{{ if for_test- }}
+{{ if is_test_mode }}
void set_{header}_{item.flag_name}(bool val);
{{ -endif }}
{{ -endfor }}
-{{ if for_test- }}
+{{ if is_test_mode }}
void {header}_reset_flags();
{{ -endif }}
diff --git a/tools/aconfig/templates/cpp_source_file.template b/tools/aconfig/templates/cpp_source_file.template
index fbbfedc..a10d7cb 100644
--- a/tools/aconfig/templates/cpp_source_file.template
+++ b/tools/aconfig/templates/cpp_source_file.template
@@ -2,9 +2,8 @@
{{ if readwrite- }}
#include <server_configurable_flags/get_flags.h>
-{{ -endif }}
-
-{{ if for_test- }}
+{{ endif }}
+{{ if is_test_mode }}
#include <unordered_map>
#include <string>
{{ -else- }}
@@ -15,7 +14,7 @@
namespace {cpp_namespace} \{
-{{ if for_test- }}
+{{ if is_test_mode }}
class flag_provider : public flag_provider_interface \{
private:
std::unordered_map<std::string, bool> overrides_;
@@ -59,6 +58,7 @@
{{ for item in class_elements }}
virtual bool {item.flag_name}() override \{
+ {{ if is_prod_mode- }}
{{ if item.readwrite- }}
if (cache_[{item.readwrite_idx}] == -1) \{
cache_[{item.readwrite_idx}] = server_configurable_flags::GetServerConfigurableFlag(
@@ -74,6 +74,17 @@
return {item.default_value};
{{ -endif }}
{{ -endif }}
+ {{ -else- }}
+ {{ if is_exported_mode-}}
+ if (cache_[{item.readwrite_idx}] == -1) \{
+ cache_[{item.readwrite_idx}] = server_configurable_flags::GetServerConfigurableFlag(
+ "aconfig_flags.{item.device_config_namespace}",
+ "{item.device_config_flag}",
+ "false") == "true";
+ }
+ return cache_[{item.readwrite_idx}];
+ {{ -endif }}
+ {{ -endif }}
}
{{ endfor }}
{{ if readwrite- }}
@@ -91,9 +102,10 @@
{{ for item in class_elements }}
bool {header}_{item.flag_name}() \{
- {{ if for_test- }}
+ {{ if is_test_mode }}
return {cpp_namespace}::{item.flag_name}();
{{ -else- }}
+ {{ if is_prod_mode- }}
{{ if item.readwrite- }}
return {cpp_namespace}::{item.flag_name}();
{{ -else- }}
@@ -103,17 +115,22 @@
return {item.default_value};
{{ -endif }}
{{ -endif }}
+ {{ -else- }}
+ {{ if is_exported_mode- }}
+ return {cpp_namespace}::{item.flag_name}();
+ {{ -endif }}
+ {{ -endif }}
{{ -endif }}
}
-{{ if for_test- }}
+{{ if is_test_mode }}
void set_{header}_{item.flag_name}(bool val) \{
{cpp_namespace}::{item.flag_name}(val);
}
{{ -endif }}
{{ endfor-}}
-{{ if for_test }}
+{{ if is_test_mode }}
void {header}_reset_flags() \{
{cpp_namespace}::reset_flags();
}
diff --git a/tools/aconfig/templates/rust_exported.template b/tools/aconfig/templates/rust_exported.template
new file mode 100644
index 0000000..b31bcef
--- /dev/null
+++ b/tools/aconfig/templates/rust_exported.template
@@ -0,0 +1,36 @@
+//! codegenerated rust flag lib
+
+/// flag provider
+pub struct FlagProvider;
+
+lazy_static::lazy_static! \{
+ {{ for flag in template_flags }}
+ /// flag value cache for {flag.name}
+ static ref CACHED_{flag.name}: bool = flags_rust::GetServerConfigurableFlag(
+ "aconfig_flags.{flag.device_config_namespace}",
+ "{flag.device_config_flag}",
+ "false") == "true";
+ {{ endfor }}
+}
+
+impl FlagProvider \{
+
+ {{ for flag in template_flags }}
+ /// query flag {flag.name}
+ pub fn {flag.name}(&self) -> bool \{
+ *CACHED_{flag.name}
+ }
+ {{ endfor }}
+
+}
+
+/// flag provider
+pub static PROVIDER: FlagProvider = FlagProvider;
+
+{{ for flag in template_flags }}
+/// query flag {flag.name}
+#[inline(always)]
+pub fn {flag.name}() -> bool \{
+ PROVIDER.{flag.name}()
+}
+{{ endfor }}
diff --git a/tools/aconfig/tests/aconfig_exported_mode_test.cpp b/tools/aconfig/tests/aconfig_exported_mode_test.cpp
new file mode 100644
index 0000000..d6eab43
--- /dev/null
+++ b/tools/aconfig/tests/aconfig_exported_mode_test.cpp
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+#include "com_android_aconfig_test.h"
+#include "gtest/gtest.h"
+
+using namespace com::android::aconfig::test;
+
+TEST(AconfigTest, TestDisabledRwExportedFlag) {
+ ASSERT_FALSE(com_android_aconfig_test_disabled_rw_exported());
+ ASSERT_FALSE(provider_->disabled_rw_exported());
+ ASSERT_FALSE(disabled_rw_exported());
+}
+
+TEST(AconfigTest, TestEnabledFixedRoExportedFlag) {
+ // TODO: change to assertTrue(enabledFixedRoExported()) when the build supports reading tests/*.values
+ ASSERT_FALSE(com_android_aconfig_test_enabled_fixed_ro_exported());
+ ASSERT_FALSE(provider_->enabled_fixed_ro_exported());
+ ASSERT_FALSE(enabled_fixed_ro_exported());
+}
+
+TEST(AconfigTest, TestEnabledRoExportedFlag) {
+ // TODO: change to assertTrue(enabledRoExported()) when the build supports reading tests/*.values
+ ASSERT_FALSE(com_android_aconfig_test_enabled_ro_exported());
+ ASSERT_FALSE(provider_->enabled_ro_exported());
+ ASSERT_FALSE(enabled_ro_exported());
+}
+
+int main(int argc, char** argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
\ No newline at end of file
diff --git a/tools/aconfig/tests/aconfig_exported_mode_test.rs b/tools/aconfig/tests/aconfig_exported_mode_test.rs
new file mode 100644
index 0000000..4b48047
--- /dev/null
+++ b/tools/aconfig/tests/aconfig_exported_mode_test.rs
@@ -0,0 +1,7 @@
+#[cfg(not(feature = "cargo"))]
+#[test]
+fn test_flags() {
+ assert!(!aconfig_test_rust_library::disabled_rw_exported());
+ assert!(!aconfig_test_rust_library::enabled_fixed_ro_exported());
+ assert!(!aconfig_test_rust_library::enabled_ro_exported());
+}
diff --git a/tools/aconfig/tests/first.values b/tools/aconfig/tests/first.values
index 731ce84..2efb463 100644
--- a/tools/aconfig/tests/first.values
+++ b/tools/aconfig/tests/first.values
@@ -40,3 +40,9 @@
state: DISABLED
permission: READ_WRITE
}
+flag_value {
+ package: "com.android.aconfig.test"
+ name: "enabled_fixed_ro_exported"
+ state: ENABLED
+ permission: READ_ONLY
+}
diff --git a/tools/aconfig/tests/test.aconfig b/tools/aconfig/tests/test.aconfig
index 014bced..c11508a 100644
--- a/tools/aconfig/tests/test.aconfig
+++ b/tools/aconfig/tests/test.aconfig
@@ -78,3 +78,12 @@
bug: "111"
is_exported: true
}
+
+flag {
+ name: "enabled_fixed_ro_exported"
+ namespace: "aconfig_test"
+ description: "This flag is fixed ENABLED + READ_ONLY and exported"
+ bug: "111"
+ is_fixed_read_only: true
+ is_exported: true
+}
\ No newline at end of file
diff --git a/tools/perf/benchmarks b/tools/perf/benchmarks
new file mode 100755
index 0000000..c42a2d8
--- /dev/null
+++ b/tools/perf/benchmarks
@@ -0,0 +1,550 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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
+#
+# http://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.
+
+import sys
+if __name__ == "__main__":
+ sys.dont_write_bytecode = True
+
+import argparse
+import dataclasses
+import datetime
+import json
+import os
+import pathlib
+import shutil
+import subprocess
+import time
+
+import pretty
+import utils
+
+
+class FatalError(Exception):
+ def __init__(self):
+ pass
+
+
+class OptionsError(Exception):
+ def __init__(self, message):
+ self.message = message
+
+
+@dataclasses.dataclass(frozen=True)
+class Lunch:
+ "Lunch combination"
+
+ target_product: str
+ "TARGET_PRODUCT"
+
+ target_release: str
+ "TARGET_RELEASE"
+
+ target_build_variant: str
+ "TARGET_BUILD_VARIANT"
+
+ def ToDict(self):
+ return {
+ "TARGET_PRODUCT": self.target_product,
+ "TARGET_RELEASE": self.target_release,
+ "TARGET_BUILD_VARIANT": self.target_build_variant,
+ }
+
+ def Combine(self):
+ return f"{self.target_product}-{self.target_release}-{self.target_build_variant}"
+
+
+@dataclasses.dataclass(frozen=True)
+class Change:
+ "A change that we make to the tree, and how to undo it"
+ label: str
+ "String to print in the log when the change is made"
+
+ change: callable
+ "Function to change the source tree"
+
+ undo: callable
+ "Function to revert the source tree to its previous condition in the most minimal way possible."
+
+
+@dataclasses.dataclass(frozen=True)
+class Benchmark:
+ "Something we measure"
+
+ id: str
+ "Short ID for the benchmark, for the command line"
+
+ title: str
+ "Title for reports"
+
+ change: Change
+ "Source tree modification for the benchmark that will be measured"
+
+ modules: list[str]
+ "Build modules to build on soong command line"
+
+ preroll: int
+ "Number of times to run the build command to stabilize"
+
+ postroll: int
+ "Number of times to run the build command after reverting the action to stabilize"
+
+
+@dataclasses.dataclass(frozen=True)
+class FileSnapshot:
+ "Snapshot of a file's contents."
+
+ filename: str
+ "The file that was snapshottened"
+
+ contents: str
+ "The contents of the file"
+
+ def write(self):
+ "Write the contents back to the file"
+ with open(self.filename, "w") as f:
+ f.write(self.contents)
+
+
+def Snapshot(filename):
+ """Return a FileSnapshot with the file's current contents."""
+ with open(filename) as f:
+ contents = f.read()
+ return FileSnapshot(filename, contents)
+
+
+def Clean():
+ """Remove the out directory."""
+ def remove_out():
+ if os.path.exists("out"):
+ shutil.rmtree("out")
+ return Change(label="Remove out", change=remove_out, undo=lambda: None)
+
+
+def NoChange():
+ """No change to the source tree."""
+ return Change(label="No change", change=lambda: None, undo=lambda: None)
+
+
+def Modify(filename, contents, before=None):
+ """Create an action to modify `filename` by appending `contents` before the last instances
+ of `before` in the file.
+
+ Raises an error if `before` doesn't appear in the file.
+ """
+ orig = Snapshot(filename)
+ if before:
+ index = orig.contents.rfind(before)
+ if index < 0:
+ report_error(f"{filename}: Unable to find string '{before}' for modify operation.")
+ raise FatalError()
+ else:
+ index = len(orig.contents)
+ modified = FileSnapshot(filename, orig.contents[:index] + contents + orig.contents[index:])
+ return Change(
+ label="Modify " + filename,
+ change=lambda: modified.write(),
+ undo=lambda: orig.write()
+ )
+
+
+class BenchmarkReport():
+ "Information about a run of the benchmark"
+
+ lunch: Lunch
+ "lunch combo"
+
+ benchmark: Benchmark
+ "The benchmark object."
+
+ iteration: int
+ "Which iteration of the benchmark"
+
+ log_dir: str
+ "Path the the log directory, relative to the root of the reports directory"
+
+ preroll_duration_ns: [int]
+ "Durations of the in nanoseconds."
+
+ duration_ns: int
+ "Duration of the measured portion of the benchmark in nanoseconds."
+
+ postroll_duration_ns: [int]
+ "Durations of the postrolls in nanoseconds."
+
+ complete: bool
+ "Whether the benchmark made it all the way through the postrolls."
+
+ def __init__(self, lunch, benchmark, iteration, log_dir):
+ self.lunch = lunch
+ self.benchmark = benchmark
+ self.iteration = iteration
+ self.log_dir = log_dir
+ self.preroll_duration_ns = []
+ self.duration_ns = -1
+ self.postroll_duration_ns = []
+ self.complete = False
+
+ def ToDict(self):
+ return {
+ "lunch": self.lunch.ToDict(),
+ "id": self.benchmark.id,
+ "title": self.benchmark.title,
+ "modules": self.benchmark.modules,
+ "change": self.benchmark.change.label,
+ "iteration": self.iteration,
+ "log_dir": self.log_dir,
+ "preroll_duration_ns": self.preroll_duration_ns,
+ "duration_ns": self.duration_ns,
+ "postroll_duration_ns": self.postroll_duration_ns,
+ "complete": self.complete,
+ }
+
+class Runner():
+ """Runs the benchmarks."""
+
+ def __init__(self, options):
+ self._options = options
+ self._reports = []
+ self._complete = False
+
+ def Run(self):
+ """Run all of the user-selected benchmarks."""
+ # Clean out the log dir or create it if necessary
+ prepare_log_dir(self._options.LogDir())
+
+ try:
+ for lunch in self._options.Lunches():
+ print(lunch)
+ for benchmark in self._options.Benchmarks():
+ for iteration in range(self._options.Iterations()):
+ self._run_benchmark(lunch, benchmark, iteration)
+ self._complete = True
+ finally:
+ self._write_summary()
+
+
+ def _run_benchmark(self, lunch, benchmark, iteration):
+ """Run a single benchmark."""
+ benchmark_log_subdir = self._log_dir(lunch, benchmark, iteration)
+ benchmark_log_dir = self._options.LogDir().joinpath(benchmark_log_subdir)
+
+ sys.stderr.write(f"STARTING BENCHMARK: {benchmark.id}\n")
+ sys.stderr.write(f" lunch: {lunch.Combine()}\n")
+ sys.stderr.write(f" iteration: {iteration}\n")
+ sys.stderr.write(f" benchmark_log_dir: {benchmark_log_dir}\n")
+
+ report = BenchmarkReport(lunch, benchmark, iteration, benchmark_log_subdir)
+ self._reports.append(report)
+
+ # Preroll builds
+ for i in range(benchmark.preroll):
+ ns = self._run_build(lunch, benchmark_log_dir.joinpath(f"pre_{i}"), benchmark.modules)
+ report.preroll_duration_ns.append(ns)
+
+ sys.stderr.write(f"PERFORMING CHANGE: {benchmark.change.label}\n")
+ if not self._options.DryRun():
+ benchmark.change.change()
+ try:
+
+ # Measured build
+ ns = self._run_build(lunch, benchmark_log_dir.joinpath("measured"), benchmark.modules)
+ report.duration_ns = ns
+
+ # Postroll builds
+ for i in range(benchmark.preroll):
+ ns = self._run_build(lunch, benchmark_log_dir.joinpath(f"post_{i}"),
+ benchmark.modules)
+ report.postroll_duration_ns.append(ns)
+
+ finally:
+ # Always undo, even if we crashed or the build failed and we stopped.
+ sys.stderr.write(f"UNDOING CHANGE: {benchmark.change.label}\n")
+ if not self._options.DryRun():
+ benchmark.change.undo()
+
+ self._write_summary()
+ sys.stderr.write(f"FINISHED BENCHMARK: {benchmark.id}\n")
+
+ def _log_dir(self, lunch, benchmark, iteration):
+ """Construct the log directory fir a benchmark run."""
+ path = f"{lunch.Combine()}/{benchmark.id}"
+ # Zero pad to the correct length for correct alpha sorting
+ path += ("/%0" + str(len(str(self._options.Iterations()))) + "d") % iteration
+ return path
+
+ def _run_build(self, lunch, build_log_dir, modules):
+ """Builds the modules. Saves interesting log files to log_dir. Raises FatalError
+ if the build fails.
+ """
+ sys.stderr.write(f"STARTING BUILD {modules}\n")
+
+ before_ns = time.perf_counter_ns()
+ if not self._options.DryRun():
+ cmd = [
+ "build/soong/soong_ui.bash",
+ "--build-mode",
+ "--all-modules",
+ f"--dir={self._options.root}",
+ ] + modules
+ env = dict(os.environ)
+ env["TARGET_PRODUCT"] = lunch.target_product
+ env["TARGET_RELEASE"] = lunch.target_release
+ env["TARGET_BUILD_VARIANT"] = lunch.target_build_variant
+ returncode = subprocess.call(cmd, env=env)
+ if returncode != 0:
+ report_error(f"Build failed: {' '.join(cmd)}")
+ raise FatalError()
+
+ after_ns = time.perf_counter_ns()
+
+ # TODO: Copy some log files.
+
+ sys.stderr.write(f"FINISHED BUILD {modules}\n")
+
+ return after_ns - before_ns
+
+ def _write_summary(self):
+ # Write the results, even if the build failed or we crashed, including
+ # whether we finished all of the benchmarks.
+ data = {
+ "start_time": self._options.Timestamp().isoformat(),
+ "branch": self._options.Branch(),
+ "tag": self._options.Tag(),
+ "benchmarks": [report.ToDict() for report in self._reports],
+ "complete": self._complete,
+ }
+ with open(self._options.LogDir().joinpath("summary.json"), "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2, sort_keys=True)
+
+
+def benchmark_table(benchmarks):
+ rows = [("ID", "DESCRIPTION", "REBUILD"),]
+ rows += [(benchmark.id, benchmark.title, " ".join(benchmark.modules)) for benchmark in
+ benchmarks]
+ return rows
+
+
+def prepare_log_dir(directory):
+ if os.path.exists(directory):
+ # If it exists and isn't a directory, fail.
+ if not os.path.isdir(directory):
+ report_error(f"Log directory already exists but isn't a directory: {directory}")
+ raise FatalError()
+ # Make sure the directory is empty. Do this rather than deleting it to handle
+ # symlinks cleanly.
+ for filename in os.listdir(directory):
+ entry = os.path.join(directory, filename)
+ if os.path.isdir(entry):
+ shutil.rmtree(entry)
+ else:
+ os.unlink(entry)
+ else:
+ # Create it
+ os.makedirs(directory)
+
+
+class Options():
+ def __init__(self):
+ self._had_error = False
+
+ # Wall time clock when we started
+ self._timestamp = datetime.datetime.now(datetime.timezone.utc)
+
+ # Move to the root of the tree right away. Everything must happen from there.
+ self.root = utils.get_root()
+ if not self.root:
+ report_error("Unable to find root of tree from cwd.")
+ raise FatalError()
+ os.chdir(self.root)
+
+ # Initialize the Benchmarks. Note that this pre-loads all of the files, etc.
+ # Doing all that here forces us to fail fast if one of them can't load a required
+ # file, at the cost of a small startup speed. Don't make this do something slow
+ # like scan the whole tree.
+ self._init_benchmarks()
+
+ # Argument parsing
+ epilog = f"""
+benchmarks:
+{pretty.FormatTable(benchmark_table(self._benchmarks), prefix=" ")}
+"""
+
+ parser = argparse.ArgumentParser(
+ prog="benchmarks",
+ allow_abbrev=False, # Don't let people write unsupportable scripts.
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=epilog,
+ description="Run build system performance benchmarks.")
+ self.parser = parser
+
+ parser.add_argument("--log-dir",
+ help="Directory for logs. Default is $TOP/../benchmarks/.")
+ parser.add_argument("--dated-logs", action="store_true",
+ help="Append timestamp to log dir.")
+ parser.add_argument("-n", action="store_true", dest="dry_run",
+ help="Dry run. Don't run the build commands but do everything else.")
+ parser.add_argument("--tag",
+ help="Variant of the run, for when there are multiple perf runs.")
+ parser.add_argument("--lunch", nargs="*",
+ help="Lunch combos to test")
+ parser.add_argument("--iterations", type=int, default=1,
+ help="Number of iterations of each test to run.")
+ parser.add_argument("--branch", type=str,
+ help="Specify branch. Otherwise a guess will be made based on repo.")
+ parser.add_argument("--benchmark", nargs="*", default=[b.id for b in self._benchmarks],
+ metavar="BENCHMARKS",
+ help="Benchmarks to run. Default suite will be run if omitted.")
+
+ self._args = parser.parse_args()
+
+ self._branch = self._branch()
+ self._log_dir = self._log_dir()
+ self._lunches = self._lunches()
+
+ # Validate the benchmark ids
+ all_ids = [benchmark.id for benchmark in self._benchmarks]
+ bad_ids = [id for id in self._args.benchmark if id not in all_ids]
+ if bad_ids:
+ for id in bad_ids:
+ self._error(f"Invalid benchmark: {id}")
+
+ if self._had_error:
+ raise FatalError()
+
+ def Timestamp(self):
+ return self._timestamp
+
+ def _branch(self):
+ """Return the branch, either from the command line or by guessing from repo."""
+ if self._args.branch:
+ return self._args.branch
+ try:
+ branch = subprocess.check_output(f"cd {self.root}/.repo/manifests"
+ + " && git rev-parse --abbrev-ref --symbolic-full-name @{u}",
+ shell=True, encoding="utf-8")
+ return branch.strip().split("/")[-1]
+ except subprocess.CalledProcessError as ex:
+ report_error("Can't get branch from .repo dir. Specify --branch argument")
+ report_error(str(ex))
+ raise FatalError()
+
+ def Branch(self):
+ return self._branch
+
+ def _log_dir(self):
+ "The log directory to use, based on the current options"
+ if self._args.log_dir:
+ d = pathlib.Path(self._args.log_dir).resolve().absolute()
+ else:
+ d = self.root.joinpath("..", utils.DEFAULT_REPORT_DIR)
+ if self._args.dated_logs:
+ d = d.joinpath(self._timestamp.strftime('%Y-%m-%d'))
+ d = d.joinpath(self._branch)
+ if self._args.tag:
+ d = d.joinpath(self._args.tag)
+ return d.resolve().absolute()
+
+ def LogDir(self):
+ return self._log_dir
+
+ def Benchmarks(self):
+ return [b for b in self._benchmarks if b.id in self._args.benchmark]
+
+ def Tag(self):
+ return self._args.tag
+
+ def DryRun(self):
+ return self._args.dry_run
+
+ def _lunches(self):
+ def parse_lunch(lunch):
+ parts = lunch.split("-")
+ if len(parts) != 3:
+ raise OptionsError(f"Invalid lunch combo: {lunch}")
+ return Lunch(parts[0], parts[1], parts[2])
+ # If they gave lunch targets on the command line use that
+ if self._args.lunch:
+ result = []
+ # Split into Lunch objects
+ for lunch in self._args.lunch:
+ try:
+ result.append(parse_lunch(lunch))
+ except OptionsError as ex:
+ self._error(ex.message)
+ return result
+ # Use whats in the environment
+ product = os.getenv("TARGET_PRODUCT")
+ release = os.getenv("TARGET_RELEASE")
+ variant = os.getenv("TARGET_BUILD_VARIANT")
+ if (not product) or (not release) or (not variant):
+ # If they didn't give us anything, fail rather than guessing. There's no good
+ # default for AOSP.
+ self._error("No lunch combo specified. Either pass --lunch argument or run lunch.")
+ return []
+ return [Lunch(product, release, variant),]
+
+ def Lunches(self):
+ return self._lunches
+
+ def Iterations(self):
+ return self._args.iterations
+
+ def _init_benchmarks(self):
+ """Initialize the list of benchmarks."""
+ # Assumes that we've already chdired to the root of the tree.
+ self._benchmarks = [
+ Benchmark(id="full",
+ title="Full build",
+ change=Clean(),
+ modules=["droid"],
+ preroll=0,
+ postroll=3
+ ),
+ Benchmark(id="nochange",
+ title="No change",
+ change=NoChange(),
+ modules=["droid"],
+ preroll=2,
+ postroll=3
+ ),
+ Benchmark(id="modify_bp",
+ title="Modify Android.bp",
+ change=Modify("bionic/libc/Android.bp", "// Comment"),
+ modules=["droid"],
+ preroll=1,
+ postroll=3
+ ),
+ ]
+
+ def _error(self, message):
+ report_error(message)
+ self._had_error = True
+
+
+def report_error(message):
+ sys.stderr.write(f"error: {message}\n")
+
+
+def main(argv):
+ try:
+ options = Options()
+ runner = Runner(options)
+ runner.Run()
+ except FatalError:
+ sys.stderr.write(f"FAILED\n")
+
+
+if __name__ == "__main__":
+ main(sys.argv)
diff --git a/tools/perf/format_benchmarks b/tools/perf/format_benchmarks
new file mode 100755
index 0000000..4c1e38b
--- /dev/null
+++ b/tools/perf/format_benchmarks
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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
+#
+# http://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.
+
+import sys
+if __name__ == "__main__":
+ sys.dont_write_bytecode = True
+
+import argparse
+import dataclasses
+import datetime
+import json
+import os
+import pathlib
+import statistics
+import zoneinfo
+
+import pretty
+import utils
+
+# TODO:
+# - Flag if the last postroll build was more than 15 seconds or something. That's
+# an indicator that something is amiss.
+# - Add a mode to print all of the values for multi-iteration runs
+# - Add a flag to reorder the tags
+# - Add a flag to reorder the headers in order to show grouping more clearly.
+
+
+def FindSummaries(args):
+ def find_summaries(directory):
+ return [str(p.resolve()) for p in pathlib.Path(directory).glob("**/summary.json")]
+ if not args:
+ # If they didn't give an argument, use the default dir
+ root = utils.get_root()
+ if not root:
+ return []
+ return find_summaries(root.joinpath("..", utils.DEFAULT_REPORT_DIR))
+ results = list()
+ for arg in args:
+ if os.path.isfile(arg):
+ # If it's a file add that
+ results.append(arg)
+ elif os.path.isdir(arg):
+ # If it's a directory, find all of the files there
+ results += find_summaries(arg)
+ else:
+ sys.stderr.write(f"Invalid summary argument: {arg}\n")
+ sys.exit(1)
+ return sorted(list(results))
+
+
+def LoadSummary(filename):
+ with open(filename) as f:
+ return json.load(f)
+
+# Columns:
+# Date
+# Branch
+# Tag
+# --
+# Lunch
+# Rows:
+# Benchmark
+
+@dataclasses.dataclass(frozen=True)
+class Key():
+ pass
+
+class Column():
+ def __init__(self):
+ pass
+
+def lunch_str(d):
+ "Convert a lunch dict to a string"
+ return f"{d['TARGET_PRODUCT']}-{d['TARGET_RELEASE']}-{d['TARGET_BUILD_VARIANT']}"
+
+def group_by(l, key):
+ "Return a list of tuples, grouped by key, sorted by key"
+ result = {}
+ for item in l:
+ result.setdefault(key(item), []).append(item)
+ return [(k, v) for k, v in result.items()]
+
+
+class Table:
+ def __init__(self):
+ self._data = {}
+ self._rows = []
+ self._cols = []
+
+ def Set(self, column_key, row_key, data):
+ self._data[(column_key, row_key)] = data
+ if not column_key in self._cols:
+ self._cols.append(column_key)
+ if not row_key in self._rows:
+ self._rows.append(row_key)
+
+ def Write(self, out):
+ table = []
+ # Expand the column items
+ for row in zip(*self._cols):
+ if row.count(row[0]) == len(row):
+ continue
+ table.append([""] + [col for col in row])
+ if table:
+ table.append(pretty.SEPARATOR)
+ # Populate the data
+ for row in self._rows:
+ table.append([str(row)] + [str(self._data.get((col, row), "")) for col in self._cols])
+ out.write(pretty.FormatTable(table))
+
+
+def format_duration_sec(ns):
+ "Format a duration in ns to second precision"
+ sec = round(ns / 1000000000)
+ h, sec = divmod(sec, 60*60)
+ m, sec = divmod(sec, 60)
+ result = ""
+ if h > 0:
+ result += f"{h:2d}h "
+ if h > 0 or m > 0:
+ result += f"{m:2d}m "
+ return result + f"{sec:2d}s"
+
+def main(argv):
+ parser = argparse.ArgumentParser(
+ prog="format_benchmarks",
+ allow_abbrev=False, # Don't let people write unsupportable scripts.
+ description="Print analysis tables for benchmarks")
+
+ parser.add_argument("summaries", nargs="*",
+ help="A summary.json file or a directory in which to look for summaries.")
+
+ args = parser.parse_args()
+
+ # Load the summaries
+ summaries = [(s, LoadSummary(s)) for s in FindSummaries(args.summaries)]
+
+ # Convert to MTV time
+ for filename, s in summaries:
+ dt = datetime.datetime.fromisoformat(s["start_time"])
+ dt = dt.astimezone(zoneinfo.ZoneInfo("America/Los_Angeles"))
+ s["datetime"] = dt
+ s["date"] = datetime.date(dt.year, dt.month, dt.day)
+
+ # Sort the summaries
+ summaries.sort(key=lambda s: (s[1]["date"], s[1]["branch"], s[1]["tag"]))
+
+ # group the benchmarks by column and iteration
+ def bm_key(b):
+ return (
+ lunch_str(b["lunch"]),
+ )
+ for filename, summary in summaries:
+ summary["columns"] = [(key, group_by(bms, lambda b: b["id"])) for key, bms
+ in group_by(summary["benchmarks"], bm_key)]
+
+ # Build the table
+ table = Table()
+ for filename, summary in summaries:
+ for key, column in summary["columns"]:
+ for id, cell in column:
+ duration_ns = statistics.median([b["duration_ns"] for b in cell])
+ table.Set(tuple([summary["date"].strftime("YYYY-MM-DD"),
+ summary["branch"],
+ summary["tag"]]
+ + list(key)),
+ cell[0]["title"], format_duration_sec(duration_ns))
+
+ table.Write(sys.stdout)
+
+if __name__ == "__main__":
+ main(sys.argv)
+
diff --git a/tools/perf/pretty.py b/tools/perf/pretty.py
new file mode 100644
index 0000000..1b59098
--- /dev/null
+++ b/tools/perf/pretty.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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
+#
+# http://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.
+
+# Formatting utilities
+
+class Sentinel():
+ pass
+
+SEPARATOR = Sentinel()
+
+def FormatTable(data, prefix=""):
+ """Pretty print a table.
+
+ Prefixes each row with `prefix`.
+ """
+ if not data:
+ return ""
+ widths = [max([len(x) if x else 0 for x in col]) for col
+ in zip(*[d for d in data if not isinstance(d, Sentinel)])]
+ result = ""
+ colsep = " "
+ for row in data:
+ result += prefix
+ if row == SEPARATOR:
+ for w in widths:
+ result += "-" * w
+ result += colsep
+ result += "\n"
+ else:
+ for i in range(len(row)):
+ cell = row[i] if row[i] else ""
+ if i != 0:
+ result += " " * (widths[i] - len(cell))
+ result += cell
+ if i == 0:
+ result += " " * (widths[i] - len(cell))
+ result += colsep
+ result += "\n"
+ return result
+
+
diff --git a/tools/perf/utils.py b/tools/perf/utils.py
new file mode 100644
index 0000000..08e393f
--- /dev/null
+++ b/tools/perf/utils.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# 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
+#
+# http://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.
+
+import os
+import pathlib
+
+DEFAULT_REPORT_DIR = "benchmarks"
+
+def get_root():
+ top_dir = os.environ.get("ANDROID_BUILD_TOP")
+ if top_dir:
+ return pathlib.Path(top_dir).resolve()
+ d = pathlib.Path.cwd()
+ while True:
+ if d.joinpath("build", "soong", "soong_ui.bash").exists():
+ return d.resolve().absolute()
+ d = d.parent
+ if d == pathlib.Path("/"):
+ return None