Merge "Detect downgrade by checking build time for all partitions" into main
diff --git a/core/Makefile b/core/Makefile
index 7d7457e..3f0da8b 100644
--- a/core/Makefile
+++ b/core/Makefile
@@ -5626,7 +5626,12 @@
FASTBOOT_INFO_VERSION = 1
INSTALLED_FASTBOOT_INFO_TARGET := $(PRODUCT_OUT)/fastboot-info.txt
-
+ifdef TARGET_BOARD_FASTBOOT_INFO_FILE
+$(INSTALLED_FASTBOOT_INFO_TARGET): $(TARGET_BOARD_FASTBOOT_INFO_FILE)
+ rm -f $@
+ $(call pretty,"Target fastboot-info.txt: $@")
+ $(hide) cp $< $@
+else
$(INSTALLED_FASTBOOT_INFO_TARGET):
rm -f $@
$(call pretty,"Target fastboot-info.txt: $@")
@@ -5685,6 +5690,7 @@
ifeq ($(BOARD_USES_METADATA_PARTITION),true)
$(hide) echo "if-wipe erase metadata" >> $@
endif
+endif
# -----------------------------------------------------------------
# misc_info.txt
@@ -6744,7 +6750,7 @@
$(BUILT_TARGET_FILES_PACKAGE): $(BUILT_TARGET_FILES_DIR)
@echo "Packaging target files: $@"
- $(hide) $(SOONG_ZIP) -d -o $@ -C $(zip_root) -r $@.list
+ $(hide) $(SOONG_ZIP) -d -o $@ -C $(zip_root) -r $@.list -sha256
.PHONY: target-files-package
target-files-package: $(BUILT_TARGET_FILES_PACKAGE)
diff --git a/tools/aconfig/Android.bp b/tools/aconfig/Android.bp
index 82bfa7e..37be2dd 100644
--- a/tools/aconfig/Android.bp
+++ b/tools/aconfig/Android.bp
@@ -44,7 +44,6 @@
protos: ["protos/aconfig.proto"],
crate_name: "aconfig_protos",
source_stem: "aconfig_protos",
- use_protobuf3: true,
host_supported: true,
}
@@ -174,6 +173,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 +209,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 +258,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/TEST_MAPPING b/tools/aconfig/TEST_MAPPING
index e29918f..de8d932 100644
--- a/tools/aconfig/TEST_MAPPING
+++ b/tools/aconfig/TEST_MAPPING
@@ -24,6 +24,10 @@
"name": "aconfig.test"
},
{
+ // aconfig Java integration tests (host)
+ "name": "AconfigJavaHostTest"
+ },
+ {
// aconfig Java integration tests
"name": "aconfig.test.java"
},
@@ -36,14 +40,22 @@
"name": "aconfig.test.cpp.test_mode"
},
{
- // aconfig C++ integration tests (production mode auto-generated code)
+ // aconfig C++ integration tests (exported mode auto-generated code)
+ "name": "aconfig.test.cpp.exported_mode"
+ },
+ {
+ // aconfig Rust integration tests (production mode auto-generated code)
"name": "aconfig.prod_mode.test.rust"
},
{
- // aconfig C++ integration tests (test mode auto-generated code)
+ // aconfig Rust integration tests (test mode auto-generated code)
"name": "aconfig.test_mode.test.rust"
},
{
+ // aconfig Rust integration tests (exported mode auto-generated code)
+ "name": "aconfig.exported_mode.test.rust"
+ },
+ {
// printflags unit tests
"name": "printflags.test"
}
diff --git a/tools/aconfig/src/codegen/cpp.rs b/tools/aconfig/src/codegen/cpp.rs
index 000581b..d6bebba 100644
--- a/tools/aconfig/src/codegen/cpp.rs
+++ b/tools/aconfig/src/codegen/cpp.rs
@@ -20,20 +20,21 @@
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<'a, I>(
+pub fn generate_cpp_code<I>(
package: &str,
parsed_flags_iter: I,
codegen_mode: CodegenMode,
) -> Result<Vec<OutputFile>>
where
- I: Iterator<Item = &'a ProtoParsedFlag>,
+ I: Iterator<Item = ProtoParsedFlag>,
{
let mut readwrite_count = 0;
let class_elements: Vec<ClassElement> = parsed_flags_iter
- .map(|pf| create_class_element(package, pf, &mut readwrite_count))
+ .map(|pf| create_class_element(package, &pf, &mut readwrite_count))
.collect();
let readwrite = readwrite_count > 0;
let has_fixed_read_only = class_elements.iter().any(|item| item.is_fixed_read_only);
@@ -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>,
}
@@ -135,6 +140,7 @@
#[cfg(test)]
mod tests {
use super::*;
+ use crate::protos::ProtoParsedFlags;
use std::collections::HashMap;
const EXPORTED_PROD_HEADER_EXPECTED: &str = r#"
@@ -148,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>
@@ -168,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;
@@ -197,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;
}
@@ -224,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();
@@ -269,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;
@@ -326,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();
}
@@ -379,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);
@@ -401,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>
@@ -449,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;
}
@@ -495,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;
}
@@ -600,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()) {
@@ -696,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();
@@ -732,10 +844,193 @@
"#;
- fn test_generate_cpp_code(mode: CodegenMode) {
- let parsed_flags = crate::test::parse_test_flags();
+ 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
+
+#ifndef COM_ANDROID_ACONFIG_TEST
+#define COM_ANDROID_ACONFIG_TEST(FLAG) COM_ANDROID_ACONFIG_TEST_##FLAG
+#endif
+
+#ifndef COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO
+#define COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO false
+#endif
+
+#ifndef COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO
+#define COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO true
+#endif
+
+#ifdef __cplusplus
+
+#include <memory>
+
+namespace com::android::aconfig::test {
+
+class flag_provider_interface {
+public:
+ virtual ~flag_provider_interface() = default;
+
+ virtual bool disabled_fixed_ro() = 0;
+
+ virtual bool disabled_ro() = 0;
+
+ virtual bool enabled_fixed_ro() = 0;
+
+ virtual bool enabled_ro() = 0;
+};
+
+extern std::unique_ptr<flag_provider_interface> provider_;
+
+inline bool disabled_fixed_ro() {
+ return COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO;
+}
+
+inline bool disabled_ro() {
+ return false;
+}
+
+inline bool enabled_fixed_ro() {
+ return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+}
+
+inline bool enabled_ro() {
+ return true;
+}
+}
+
+extern "C" {
+#endif // __cplusplus
+
+bool com_android_aconfig_test_disabled_fixed_ro();
+
+bool com_android_aconfig_test_disabled_ro();
+
+bool com_android_aconfig_test_enabled_fixed_ro();
+
+bool com_android_aconfig_test_enabled_ro();
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+"#;
+
+ const READ_ONLY_PROD_SOURCE_FILE_EXPECTED: &str = r#"
+#include "com_android_aconfig_test.h"
+
+namespace com::android::aconfig::test {
+
+ class flag_provider : public flag_provider_interface {
+ public:
+
+ virtual bool disabled_fixed_ro() override {
+ return COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO;
+ }
+
+ virtual bool disabled_ro() override {
+ return false;
+ }
+
+ virtual bool enabled_fixed_ro() override {
+ return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+ }
+
+ virtual bool enabled_ro() override {
+ return true;
+ }
+ };
+
+ std::unique_ptr<flag_provider_interface> provider_ =
+ std::make_unique<flag_provider>();
+}
+
+bool com_android_aconfig_test_disabled_fixed_ro() {
+ return COM_ANDROID_ACONFIG_TEST_DISABLED_FIXED_RO;
+}
+
+bool com_android_aconfig_test_disabled_ro() {
+ return false;
+}
+
+bool com_android_aconfig_test_enabled_fixed_ro() {
+ return COM_ANDROID_ACONFIG_TEST_ENABLED_FIXED_RO;
+}
+
+bool com_android_aconfig_test_enabled_ro() {
+ return true;
+}
+"#;
+
+ fn test_generate_cpp_code(
+ parsed_flags: ProtoParsedFlags,
+ mode: CodegenMode,
+ expected_header: &str,
+ expected_src: &str,
+ ) {
+ let modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
let generated =
- generate_cpp_code(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter(), mode)
+ 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 {
@@ -750,12 +1045,7 @@
assert_eq!(
None,
crate::test::first_significant_code_diff(
- match mode {
- CodegenMode::Production => EXPORTED_PROD_HEADER_EXPECTED,
- CodegenMode::Test => EXPORTED_TEST_HEADER_EXPECTED,
- CodegenMode::Exported =>
- todo!("exported mode not yet supported for cpp, see b/313894653."),
- },
+ expected_header,
generated_files_map.get(&target_file_path).unwrap()
)
);
@@ -765,12 +1055,7 @@
assert_eq!(
None,
crate::test::first_significant_code_diff(
- match mode {
- CodegenMode::Production => PROD_SOURCE_FILE_EXPECTED,
- CodegenMode::Test => TEST_SOURCE_FILE_EXPECTED,
- CodegenMode::Exported =>
- todo!("exported mode not yet supported for cpp, see b/313894653."),
- },
+ expected_src,
generated_files_map.get(&target_file_path).unwrap()
)
);
@@ -778,11 +1063,45 @@
#[test]
fn test_generate_cpp_code_for_prod() {
- test_generate_cpp_code(CodegenMode::Production);
+ let parsed_flags = crate::test::parse_test_flags();
+ test_generate_cpp_code(
+ parsed_flags,
+ CodegenMode::Production,
+ EXPORTED_PROD_HEADER_EXPECTED,
+ PROD_SOURCE_FILE_EXPECTED,
+ );
}
#[test]
fn test_generate_cpp_code_for_test() {
- test_generate_cpp_code(CodegenMode::Test);
+ let parsed_flags = crate::test::parse_test_flags();
+ test_generate_cpp_code(
+ parsed_flags,
+ CodegenMode::Test,
+ EXPORTED_TEST_HEADER_EXPECTED,
+ TEST_SOURCE_FILE_EXPECTED,
+ );
+ }
+
+ #[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(
+ parsed_flags,
+ CodegenMode::Production,
+ READ_ONLY_EXPORTED_PROD_HEADER_EXPECTED,
+ READ_ONLY_PROD_SOURCE_FILE_EXPECTED,
+ );
}
}
diff --git a/tools/aconfig/src/codegen/java.rs b/tools/aconfig/src/codegen/java.rs
index ae3f274..a02a7e2 100644
--- a/tools/aconfig/src/codegen/java.rs
+++ b/tools/aconfig/src/codegen/java.rs
@@ -14,28 +14,27 @@
* limitations under the License.
*/
-use anyhow::{anyhow, Result};
+use anyhow::Result;
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
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<'a, I>(
+pub fn generate_java_code<I>(
package: &str,
parsed_flags_iter: I,
codegen_mode: CodegenMode,
) -> Result<Vec<OutputFile>>
where
- I: Iterator<Item = &'a ProtoParsedFlag>,
+ I: Iterator<Item = ProtoParsedFlag>,
{
let flag_elements: Vec<FlagElement> =
- parsed_flags_iter.map(|pf| create_flag_element(package, pf)).collect();
- let exported_flag_elements: Vec<FlagElement> =
- flag_elements.iter().filter(|elem| elem.exported).cloned().collect();
+ parsed_flags_iter.map(|pf| create_flag_element(package, &pf)).collect();
let namespace_flags = gen_flags_by_namespace(&flag_elements);
let properties_set: BTreeSet<String> =
flag_elements.iter().map(|fe| format_property_name(&fe.device_config_namespace)).collect();
@@ -44,13 +43,8 @@
let runtime_lookup_required =
flag_elements.iter().any(|elem| elem.is_read_write) || library_exported;
- if library_exported && exported_flag_elements.is_empty() {
- return Err(anyhow!("exported library contains no exported flags"));
- }
-
let context = Context {
flag_elements,
- exported_flag_elements,
namespace_flags,
is_test_mode,
runtime_lookup_required,
@@ -109,7 +103,6 @@
#[derive(Serialize)]
struct Context {
pub flag_elements: Vec<FlagElement>,
- pub exported_flag_elements: Vec<FlagElement>,
pub namespace_flags: Vec<NamespaceFlags>,
pub is_test_mode: bool,
pub runtime_lookup_required: bool,
@@ -133,7 +126,6 @@
pub is_read_write: bool,
pub method_name: String,
pub properties: String,
- pub exported: bool,
}
fn create_flag_element(package: &str, pf: &ProtoParsedFlag) -> FlagElement {
@@ -147,7 +139,6 @@
is_read_write: pf.permission() == ProtoFlagPermission::READ_WRITE,
method_name: format_java_method_name(pf.name()),
properties: format_property_name(pf.namespace()),
- exported: pf.is_exported.unwrap_or(false),
}
}
@@ -202,6 +193,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 +222,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 +254,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 +311,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 +354,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)
@@ -359,12 +366,12 @@
#[test]
fn test_generate_java_code_production() {
let parsed_flags = crate::test::parse_test_flags();
- let generated_files = generate_java_code(
- crate::test::TEST_PACKAGE,
- parsed_flags.parsed_flag.iter(),
- CodegenMode::Production,
- )
- .unwrap();
+ let mode = CodegenMode::Production;
+ let modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+ let generated_files =
+ generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+ .unwrap();
let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
+ r#"
private static FeatureFlags FEATURE_FLAGS = new FeatureFlagsImpl();
@@ -463,6 +470,11 @@
}
@Override
@UnsupportedAppUsage
+ public boolean enabledFixedRoExported() {
+ return true;
+ }
+ @Override
+ @UnsupportedAppUsage
public boolean enabledRo() {
return true;
}
@@ -512,12 +524,12 @@
#[test]
fn test_generate_java_code_exported() {
let parsed_flags = crate::test::parse_test_flags();
- let generated_files = generate_java_code(
- crate::test::TEST_PACKAGE,
- parsed_flags.parsed_flag.iter(),
- CodegenMode::Exported,
- )
- .unwrap();
+ let mode = CodegenMode::Exported;
+ let modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+ let generated_files =
+ generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+ .unwrap();
let expect_flags_content = r#"
package com.android.aconfig.test;
@@ -528,6 +540,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 +549,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 +569,8 @@
@UnsupportedAppUsage
boolean disabledRwExported();
@UnsupportedAppUsage
+ boolean enabledFixedRoExported();
+ @UnsupportedAppUsage
boolean enabledRoExported();
}
"#;
@@ -564,8 +584,8 @@
/** @hide */
public final class FeatureFlagsImpl implements FeatureFlags {
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 +594,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) {
@@ -589,22 +611,6 @@
aconfig_test_is_cached = true;
}
- private void load_overrides_other_namespace() {
- try {
- Properties properties = DeviceConfig.getProperties("other_namespace");
- } catch (NullPointerException e) {
- throw new RuntimeException(
- "Cannot read value from namespace other_namespace "
- + "from DeviceConfig. It could be that the code using flag "
- + "executed before SettingsProvider initialization. Please use "
- + "fixed read-only flag by adding is_fixed_read_only: true in "
- + "flag declaration.",
- e
- );
- }
- other_namespace_is_cached = true;
- }
-
@Override
@UnsupportedAppUsage
public boolean disabledRwExported() {
@@ -616,6 +622,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 +657,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 +686,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)
)
);
@@ -703,12 +724,12 @@
#[test]
fn test_generate_java_code_test() {
let parsed_flags = crate::test::parse_test_flags();
- let generated_files = generate_java_code(
- crate::test::TEST_PACKAGE,
- parsed_flags.parsed_flag.iter(),
- CodegenMode::Test,
- )
- .unwrap();
+ let mode = CodegenMode::Test;
+ let modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
+ let generated_files =
+ generate_java_code(crate::test::TEST_PACKAGE, modified_parsed_flags.into_iter(), mode)
+ .unwrap();
let expect_flags_content = EXPECTED_FLAG_COMMON_CONTENT.to_string()
+ r#"
@@ -759,6 +780,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..476d2b3 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,23 @@
Ok(format!("{}.{}", package, flag_name))
}
+#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
+pub enum CodegenMode {
+ Exported,
+ Production,
+ Test,
+}
+
+impl std::fmt::Display for CodegenMode {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ CodegenMode::Exported => write!(f, "exported"),
+ CodegenMode::Production => write!(f, "production"),
+ CodegenMode::Test => write!(f, "test"),
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/tools/aconfig/src/codegen/rust.rs b/tools/aconfig/src/codegen/rust.rs
index 04be93b..56cb311 100644
--- a/tools/aconfig/src/codegen/rust.rs
+++ b/tools/aconfig/src/codegen/rust.rs
@@ -19,19 +19,20 @@
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<'a, I>(
+pub fn generate_rust_code<I>(
package: &str,
parsed_flags_iter: I,
codegen_mode: CodegenMode,
) -> Result<OutputFile>
where
- I: Iterator<Item = &'a ProtoParsedFlag>,
+ I: Iterator<Item = ProtoParsedFlag>,
{
let template_flags: Vec<TemplateParsedFlag> =
- parsed_flags_iter.map(|pf| TemplateParsedFlag::new(package, pf)).collect();
+ parsed_flags_iter.map(|pf| TemplateParsedFlag::new(package, &pf)).collect();
let has_readwrite = template_flags.iter().any(|item| item.readwrite);
let context = TemplateContext {
package: package.to_string(),
@@ -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,10 +488,78 @@
}
"#;
+ 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 modified_parsed_flags =
+ crate::commands::modify_parsed_flags_based_on_mode(parsed_flags, mode).unwrap();
let generated =
- generate_rust_code(crate::test::TEST_PACKAGE, parsed_flags.parsed_flag.iter(), mode)
+ 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!(
@@ -466,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()
)
@@ -483,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 e437c02..0c4f543 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,12 +22,13 @@
use crate::codegen::cpp::generate_cpp_code;
use crate::codegen::java::generate_java_code;
use crate::codegen::rust::generate_rust_code;
-use crate::storage::generate_storage_files;
-
+use crate::codegen::CodegenMode;
+use crate::dump::{DumpFormat, DumpPredicate};
use crate::protos::{
ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoParsedFlag,
ProtoParsedFlags, ProtoTracepoint,
};
+use crate::storage::generate_storage_files;
pub struct Input {
pub source: String,
@@ -188,38 +188,34 @@
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);
- 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");
};
- generate_java_code(package, filtered_parsed_flags.iter(), codegen_mode)
+ let package = package.to_string();
+ generate_java_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
}
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");
};
- generate_cpp_code(package, filtered_parsed_flags.iter(), codegen_mode)
+ let package = package.to_string();
+ 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");
};
- generate_rust_code(package, filtered_parsed_flags.iter(), codegen_mode)
+ let package = package.to_string();
+ generate_rust_code(&package, modified_parsed_flags.into_iter(), codegen_mode)
}
pub fn create_storage(caches: Vec<Input>, container: &str) -> Result<Vec<OutputFile>> {
@@ -276,73 +272,28 @@
Ok(output)
}
-#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
-pub enum DumpFormat {
- Text,
- Verbose,
- Protobuf,
- Textproto,
- Bool,
-}
-
pub fn dump_parsed_flags(
mut input: Vec<Input>,
format: DumpFormat,
+ filters: &[&str],
dedup: bool,
) -> Result<Vec<u8>> {
let individually_parsed_flags: Result<Vec<ProtoParsedFlags>> =
input.iter_mut().map(|i| i.try_parse_flags()).collect();
let parsed_flags: ProtoParsedFlags =
crate::protos::parsed_flags::merge(individually_parsed_flags?, dedup)?;
-
- let mut output = Vec::new();
- match format {
- DumpFormat::Text => {
- for parsed_flag in parsed_flags.parsed_flag.into_iter() {
- let line = format!(
- "{} [{}]: {:?} + {:?}\n",
- parsed_flag.fully_qualified_name(),
- parsed_flag.container(),
- parsed_flag.permission(),
- parsed_flag.state()
- );
- output.extend_from_slice(line.as_bytes());
- }
- }
- DumpFormat::Verbose => {
- for parsed_flag in parsed_flags.parsed_flag.into_iter() {
- let sources: Vec<_> =
- parsed_flag.trace.iter().map(|tracepoint| tracepoint.source()).collect();
- let line = format!(
- "{} [{}]: {:?} + {:?} ({})\n",
- parsed_flag.fully_qualified_name(),
- parsed_flag.container(),
- parsed_flag.permission(),
- parsed_flag.state(),
- sources.join(", ")
- );
- output.extend_from_slice(line.as_bytes());
- }
- }
- DumpFormat::Protobuf => {
- parsed_flags.write_to_vec(&mut output)?;
- }
- DumpFormat::Textproto => {
- let s = protobuf::text_format::print_to_string_pretty(&parsed_flags);
- output.extend_from_slice(s.as_bytes());
- }
- DumpFormat::Bool => {
- for parsed_flag in parsed_flags.parsed_flag.into_iter() {
- let line = format!(
- "{}={:?}\n",
- parsed_flag.fully_qualified_name(),
- parsed_flag.state() == ProtoFlagState::ENABLED
- );
- output.extend_from_slice(line.as_bytes());
- }
- }
- }
- Ok(output)
+ let filters: Vec<Box<DumpPredicate>> = if filters.is_empty() {
+ vec![Box::new(|_| true)]
+ } else {
+ filters
+ .iter()
+ .map(|f| crate::dump::create_filter_predicate(f))
+ .collect::<Result<Vec<_>>>()?
+ };
+ crate::dump::dump_parsed_flags(
+ parsed_flags.parsed_flag.into_iter().filter(|flag| filters.iter().any(|p| p(flag))),
+ format,
+ )
}
fn find_unique_package(parsed_flags: &[ProtoParsedFlag]) -> Option<&str> {
@@ -365,16 +316,33 @@
Some(container)
}
-fn filter_parsed_flags(
+pub fn modify_parsed_flags_based_on_mode(
parsed_flags: ProtoParsedFlags,
codegen_mode: CodegenMode,
-) -> Vec<ProtoParsedFlag> {
- match codegen_mode {
- CodegenMode::Exported => {
- parsed_flags.parsed_flag.into_iter().filter(|pf| pf.is_exported()).collect()
- }
- _ => parsed_flags.parsed_flag,
+) -> Result<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
}
+
+ let modified_parsed_flags: Vec<_> = 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()
+ }
+ };
+ if modified_parsed_flags.is_empty() {
+ bail!("{codegen_mode} library contains no {codegen_mode} flags");
+ }
+
+ Ok(modified_parsed_flags)
}
#[cfg(test)]
@@ -408,9 +376,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();
@@ -619,62 +587,33 @@
}
#[test]
- fn test_dump_text_format() {
+ fn test_dump() {
let input = parse_test_flags_as_input();
- let bytes = dump_parsed_flags(vec![input], DumpFormat::Text, false).unwrap();
- let text = std::str::from_utf8(&bytes).unwrap();
- assert!(
- text.contains("com.android.aconfig.test.disabled_ro [system]: READ_ONLY + DISABLED")
- );
- }
-
- #[test]
- fn test_dump_protobuf_format() {
- let expected = protobuf::text_format::parse_from_str::<ProtoParsedFlags>(
- crate::test::TEST_FLAGS_TEXTPROTO,
+ let bytes = dump_parsed_flags(
+ vec![input],
+ DumpFormat::Custom("{fully_qualified_name}".to_string()),
+ &[],
+ false,
)
- .unwrap()
- .write_to_bytes()
.unwrap();
-
- let input = parse_test_flags_as_input();
- let actual = dump_parsed_flags(vec![input], DumpFormat::Protobuf, false).unwrap();
-
- assert_eq!(expected, actual);
- }
-
- #[test]
- fn test_dump_textproto_format() {
- let input = parse_test_flags_as_input();
- let bytes = dump_parsed_flags(vec![input], DumpFormat::Textproto, false).unwrap();
let text = std::str::from_utf8(&bytes).unwrap();
- assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
+ assert!(text.contains("com.android.aconfig.test.disabled_ro"));
}
#[test]
fn test_dump_textproto_format_dedup() {
let input = parse_test_flags_as_input();
let input2 = parse_test_flags_as_input();
- let bytes = dump_parsed_flags(vec![input, input2], DumpFormat::Textproto, true).unwrap();
+ 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());
- }
-
- #[test]
- fn test_filter_parsed_flags() {
- let mut input = parse_test_flags_as_input();
- let parsed_flags = input.try_parse_flags().unwrap();
-
- let filtered_parsed_flags =
- filter_parsed_flags(parsed_flags.clone(), CodegenMode::Exported);
- assert_eq!(2, filtered_parsed_flags.len());
-
- let filtered_parsed_flags =
- filter_parsed_flags(parsed_flags.clone(), CodegenMode::Production);
- assert_eq!(8, 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!(
+ None,
+ crate::test::first_significant_code_diff(
+ crate::test::TEST_FLAGS_TEXTPROTO.trim(),
+ text.trim()
+ )
+ );
}
fn parse_test_flags_as_input() -> Input {
@@ -684,4 +623,36 @@
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)
+ .unwrap();
+ 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).unwrap();
+ 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());
+ }
+
+ let mut parsed_flags = crate::test::parse_test_flags();
+ parsed_flags.parsed_flag.retain_mut(|pf| !pf.is_exported());
+ let error =
+ modify_parsed_flags_based_on_mode(parsed_flags, CodegenMode::Exported).unwrap_err();
+ assert_eq!("exported library contains no exported flags", format!("{:?}", error));
+ }
}
diff --git a/tools/aconfig/src/dump.rs b/tools/aconfig/src/dump.rs
new file mode 100644
index 0000000..bcad064
--- /dev/null
+++ b/tools/aconfig/src/dump.rs
@@ -0,0 +1,433 @@
+/*
+ * 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::protos::{
+ ParsedFlagExt, ProtoFlagMetadata, ProtoFlagPermission, ProtoFlagState, ProtoTracepoint,
+};
+use crate::protos::{ProtoParsedFlag, ProtoParsedFlags};
+use anyhow::{anyhow, bail, Context, Result};
+use protobuf::Message;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum DumpFormat {
+ Protobuf,
+ Textproto,
+ Custom(String),
+}
+
+impl TryFrom<&str> for DumpFormat {
+ type Error = anyhow::Error;
+
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
+ match value {
+ // protobuf formats
+ "protobuf" => Ok(Self::Protobuf),
+ "textproto" => Ok(Self::Textproto),
+
+ // old formats now implemented as aliases to custom format
+ "text" => Ok(Self::Custom(
+ "{fully_qualified_name} [{container}]: {permission} + {state}".to_owned(),
+ )),
+ "verbose" => Ok(Self::Custom(
+ "{fully_qualified_name} [{container}]: {permission} + {state} ({trace:paths})"
+ .to_owned(),
+ )),
+ "bool" => Ok(Self::Custom("{fully_qualified_name}={state:bool}".to_owned())),
+
+ // custom format
+ _ => Ok(Self::Custom(value.to_owned())),
+ }
+ }
+}
+
+pub fn dump_parsed_flags<I>(parsed_flags_iter: I, format: DumpFormat) -> Result<Vec<u8>>
+where
+ I: Iterator<Item = ProtoParsedFlag>,
+{
+ let mut output = Vec::new();
+ match format {
+ DumpFormat::Protobuf => {
+ let parsed_flags =
+ ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() };
+ parsed_flags.write_to_vec(&mut output)?;
+ }
+ DumpFormat::Textproto => {
+ let parsed_flags =
+ ProtoParsedFlags { parsed_flag: parsed_flags_iter.collect(), ..Default::default() };
+ let s = protobuf::text_format::print_to_string_pretty(&parsed_flags);
+ output.extend_from_slice(s.as_bytes());
+ }
+ DumpFormat::Custom(format) => {
+ for flag in parsed_flags_iter {
+ dump_custom_format(&flag, &format, &mut output);
+ }
+ }
+ }
+ Ok(output)
+}
+
+fn dump_custom_format(flag: &ProtoParsedFlag, format: &str, output: &mut Vec<u8>) {
+ fn format_trace(trace: &[ProtoTracepoint]) -> String {
+ trace
+ .iter()
+ .map(|tracepoint| {
+ format!(
+ "{}: {:?} + {:?}",
+ tracepoint.source(),
+ tracepoint.permission(),
+ tracepoint.state()
+ )
+ })
+ .collect::<Vec<_>>()
+ .join(", ")
+ }
+
+ fn format_trace_paths(trace: &[ProtoTracepoint]) -> String {
+ trace.iter().map(|tracepoint| tracepoint.source()).collect::<Vec<_>>().join(", ")
+ }
+
+ fn format_metadata(metadata: &ProtoFlagMetadata) -> String {
+ format!("{:?}", metadata.purpose())
+ }
+
+ let mut str = format
+ // ProtoParsedFlag fields
+ .replace("{package}", flag.package())
+ .replace("{name}", flag.name())
+ .replace("{namespace}", flag.namespace())
+ .replace("{description}", flag.description())
+ .replace("{bug}", &flag.bug.join(", "))
+ .replace("{state}", &format!("{:?}", flag.state()))
+ .replace("{state:bool}", &format!("{}", flag.state() == ProtoFlagState::ENABLED))
+ .replace("{permission}", &format!("{:?}", flag.permission()))
+ .replace("{trace}", &format_trace(&flag.trace))
+ .replace("{trace:paths}", &format_trace_paths(&flag.trace))
+ .replace("{is_fixed_read_only}", &format!("{}", flag.is_fixed_read_only()))
+ .replace("{is_exported}", &format!("{}", flag.is_exported()))
+ .replace("{container}", flag.container())
+ .replace("{metadata}", &format_metadata(&flag.metadata))
+ // ParsedFlagExt functions
+ .replace("{fully_qualified_name}", &flag.fully_qualified_name());
+ str.push('\n');
+ output.extend_from_slice(str.as_bytes());
+}
+
+pub type DumpPredicate = dyn Fn(&ProtoParsedFlag) -> bool;
+
+pub fn create_filter_predicate(filter: &str) -> Result<Box<DumpPredicate>> {
+ let predicates = filter
+ .split('+')
+ .map(|sub_filter| create_filter_predicate_single(sub_filter))
+ .collect::<Result<Vec<_>>>()?;
+ Ok(Box::new(move |flag| predicates.iter().all(|p| p(flag))))
+}
+
+fn create_filter_predicate_single(filter: &str) -> Result<Box<DumpPredicate>> {
+ fn enum_from_str<T>(expected: &[T], s: &str) -> Result<T>
+ where
+ T: std::fmt::Debug + Copy,
+ {
+ for candidate in expected.iter() {
+ if s == format!("{:?}", candidate) {
+ return Ok(*candidate);
+ }
+ }
+ let expected =
+ expected.iter().map(|state| format!("{:?}", state)).collect::<Vec<_>>().join(", ");
+ bail!("\"{s}\": not a valid flag state, expected one of {expected}");
+ }
+
+ let error_msg = format!("\"{filter}\": filter syntax error");
+ let (what, arg) = filter.split_once(':').ok_or_else(|| anyhow!(error_msg.clone()))?;
+ match what {
+ "package" => {
+ let expected = arg.to_owned();
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.package() == expected))
+ }
+ "name" => {
+ let expected = arg.to_owned();
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.name() == expected))
+ }
+ "namespace" => {
+ let expected = arg.to_owned();
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.namespace() == expected))
+ }
+ // description: not supported yet
+ "bug" => {
+ let expected = arg.to_owned();
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.bug.join(", ") == expected))
+ }
+ "state" => {
+ let expected = enum_from_str(&[ProtoFlagState::ENABLED, ProtoFlagState::DISABLED], arg)
+ .context(error_msg)?;
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.state() == expected))
+ }
+ "permission" => {
+ let expected = enum_from_str(
+ &[ProtoFlagPermission::READ_ONLY, ProtoFlagPermission::READ_WRITE],
+ arg,
+ )
+ .context(error_msg)?;
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.permission() == expected))
+ }
+ // trace: not supported yet
+ "is_fixed_read_only" => {
+ let expected: bool = arg.parse().context(error_msg)?;
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_fixed_read_only() == expected))
+ }
+ "is_exported" => {
+ let expected: bool = arg.parse().context(error_msg)?;
+ Ok(Box::new(move |flag: &ProtoParsedFlag| flag.is_exported() == expected))
+ }
+ "container" => {
+ let expected = arg.to_owned();
+ 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)),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::protos::ProtoParsedFlags;
+ use crate::test::parse_test_flags;
+ use protobuf::Message;
+
+ fn parse_enabled_ro_flag() -> ProtoParsedFlag {
+ parse_test_flags().parsed_flag.into_iter().find(|pf| pf.name() == "enabled_ro").unwrap()
+ }
+
+ #[test]
+ fn test_dumpformat_from_str() {
+ // supported format types
+ assert_eq!(DumpFormat::try_from("protobuf").unwrap(), DumpFormat::Protobuf);
+ assert_eq!(DumpFormat::try_from("textproto").unwrap(), DumpFormat::Textproto);
+ assert_eq!(
+ DumpFormat::try_from("foobar").unwrap(),
+ DumpFormat::Custom("foobar".to_owned())
+ );
+ }
+
+ #[test]
+ fn test_dump_parsed_flags_protobuf_format() {
+ let expected = protobuf::text_format::parse_from_str::<ProtoParsedFlags>(
+ crate::test::TEST_FLAGS_TEXTPROTO,
+ )
+ .unwrap()
+ .write_to_bytes()
+ .unwrap();
+ let parsed_flags = parse_test_flags();
+ let actual =
+ dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Protobuf).unwrap();
+ assert_eq!(expected, actual);
+ }
+
+ #[test]
+ fn test_dump_parsed_flags_textproto_format() {
+ let parsed_flags = parse_test_flags();
+ let bytes =
+ dump_parsed_flags(parsed_flags.parsed_flag.into_iter(), DumpFormat::Textproto).unwrap();
+ let text = std::str::from_utf8(&bytes).unwrap();
+ assert_eq!(crate::test::TEST_FLAGS_TEXTPROTO.trim(), text.trim());
+ }
+
+ #[test]
+ fn test_dump_parsed_flags_custom_format() {
+ macro_rules! assert_dump_parsed_flags_custom_format_contains {
+ ($format:expr, $expected:expr) => {
+ let parsed_flags = parse_test_flags();
+ let bytes = dump_parsed_flags(
+ parsed_flags.parsed_flag.into_iter(),
+ $format.try_into().unwrap(),
+ )
+ .unwrap();
+ let text = std::str::from_utf8(&bytes).unwrap();
+ assert!(text.contains($expected));
+ };
+ }
+
+ // custom format
+ assert_dump_parsed_flags_custom_format_contains!(
+ "{fully_qualified_name}={permission} + {state}",
+ "com.android.aconfig.test.enabled_ro=READ_ONLY + ENABLED"
+ );
+
+ // aliases
+ assert_dump_parsed_flags_custom_format_contains!(
+ "text",
+ "com.android.aconfig.test.enabled_ro [system]: READ_ONLY + ENABLED"
+ );
+ assert_dump_parsed_flags_custom_format_contains!(
+ "verbose",
+ "com.android.aconfig.test.enabled_ro [system]: READ_ONLY + ENABLED (tests/test.aconfig, tests/first.values, tests/second.values)"
+ );
+ assert_dump_parsed_flags_custom_format_contains!(
+ "bool",
+ "com.android.aconfig.test.enabled_ro=true"
+ );
+ }
+
+ #[test]
+ fn test_dump_custom_format() {
+ macro_rules! assert_custom_format {
+ ($format:expr, $expected:expr) => {
+ let flag = parse_enabled_ro_flag();
+ let mut bytes = vec![];
+ dump_custom_format(&flag, $format, &mut bytes);
+ let text = std::str::from_utf8(&bytes).unwrap();
+ assert_eq!(text, $expected);
+ };
+ }
+
+ assert_custom_format!("{package}", "com.android.aconfig.test\n");
+ assert_custom_format!("{name}", "enabled_ro\n");
+ assert_custom_format!("{namespace}", "aconfig_test\n");
+ assert_custom_format!("{description}", "This flag is ENABLED + READ_ONLY\n");
+ assert_custom_format!("{bug}", "abc\n");
+ assert_custom_format!("{state}", "ENABLED\n");
+ assert_custom_format!("{state:bool}", "true\n");
+ assert_custom_format!("{permission}", "READ_ONLY\n");
+ assert_custom_format!("{trace}", "tests/test.aconfig: READ_WRITE + DISABLED, tests/first.values: READ_WRITE + DISABLED, tests/second.values: READ_ONLY + ENABLED\n");
+ assert_custom_format!(
+ "{trace:paths}",
+ "tests/test.aconfig, tests/first.values, tests/second.values\n"
+ );
+ assert_custom_format!("{is_fixed_read_only}", "false\n");
+ assert_custom_format!("{is_exported}", "false\n");
+ assert_custom_format!("{container}", "system\n");
+ assert_custom_format!("{metadata}", "PURPOSE_BUGFIX\n");
+
+ assert_custom_format!("name={name}|state={state}", "name=enabled_ro|state=ENABLED\n");
+ assert_custom_format!("{state}{state}{state}", "ENABLEDENABLEDENABLED\n");
+ }
+
+ #[test]
+ fn test_create_filter_predicate() {
+ macro_rules! assert_create_filter_predicate {
+ ($filter:expr, $expected:expr) => {
+ let parsed_flags = parse_test_flags();
+ let predicate = create_filter_predicate($filter).unwrap();
+ let mut filtered_flags: Vec<String> = parsed_flags
+ .parsed_flag
+ .into_iter()
+ .filter(predicate)
+ .map(|flag| flag.fully_qualified_name())
+ .collect();
+ filtered_flags.sort();
+ assert_eq!(&filtered_flags, $expected);
+ };
+ }
+
+ assert_create_filter_predicate!(
+ "package:com.android.aconfig.test",
+ &[
+ "com.android.aconfig.test.disabled_ro",
+ "com.android.aconfig.test.disabled_rw",
+ "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",
+ ]
+ );
+ assert_create_filter_predicate!(
+ "name:disabled_rw",
+ &["com.android.aconfig.test.disabled_rw"]
+ );
+ assert_create_filter_predicate!(
+ "namespace:other_namespace",
+ &["com.android.aconfig.test.disabled_rw_in_other_namespace"]
+ );
+ // description: not supported yet
+ assert_create_filter_predicate!("bug:123", &["com.android.aconfig.test.disabled_ro",]);
+ assert_create_filter_predicate!(
+ "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",
+ ]
+ );
+ assert_create_filter_predicate!(
+ "permission:READ_ONLY",
+ &[
+ "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",
+ ]
+ );
+ // 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_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",
+ ]
+ );
+ assert_create_filter_predicate!(
+ "container:system",
+ &[
+ "com.android.aconfig.test.disabled_ro",
+ "com.android.aconfig.test.disabled_rw",
+ "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",
+ ]
+ );
+ // 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 63a50c8..59f3677 100644
--- a/tools/aconfig/src/main.rs
+++ b/tools/aconfig/src/main.rs
@@ -26,13 +26,22 @@
mod codegen;
mod commands;
+mod dump;
mod protos;
mod storage;
+use codegen::CodegenMode;
+use dump::DumpFormat;
+
#[cfg(test)]
mod test;
-use commands::{CodegenMode, DumpFormat, 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
+limited to flags that match any of the filters.
+"#;
fn cli() -> Command {
Command::new("aconfig")
@@ -61,7 +70,7 @@
.arg(
Arg::new("mode")
.long("mode")
- .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+ .value_parser(EnumValueParser::<CodegenMode>::new())
.default_value("production"),
),
)
@@ -72,7 +81,7 @@
.arg(
Arg::new("mode")
.long("mode")
- .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+ .value_parser(EnumValueParser::<CodegenMode>::new())
.default_value("production"),
),
)
@@ -83,7 +92,7 @@
.arg(
Arg::new("mode")
.long("mode")
- .value_parser(EnumValueParser::<commands::CodegenMode>::new())
+ .value_parser(EnumValueParser::<CodegenMode>::new())
.default_value("production"),
),
)
@@ -98,14 +107,21 @@
.arg(Arg::new("out").long("out").default_value("-")),
)
.subcommand(
- Command::new("dump")
+ Command::new("dump-cache")
+ .alias("dump")
.arg(Arg::new("cache").long("cache").action(ArgAction::Append))
.arg(
Arg::new("format")
.long("format")
- .value_parser(EnumValueParser::<commands::DumpFormat>::new())
+ .value_parser(|s: &str| DumpFormat::try_from(s))
.default_value("text"),
)
+ .arg(
+ Arg::new("filter")
+ .long("filter")
+ .action(ArgAction::Append)
+ .help(HELP_DUMP_FILTER.trim()),
+ )
.arg(Arg::new("dedup").long("dedup").num_args(0).action(ArgAction::SetTrue))
.arg(Arg::new("out").long("out").default_value("-")),
)
@@ -245,12 +261,17 @@
let path = get_required_arg::<String>(sub_matches, "out")?;
write_output_to_file_or_stdout(path, &output)?;
}
- Some(("dump", sub_matches)) => {
+ Some(("dump-cache", sub_matches)) => {
let input = open_zero_or_more_files(sub_matches, "cache")?;
let format = get_required_arg::<DumpFormat>(sub_matches, "format")
.context("failed to dump previously parsed flags")?;
+ let filters = sub_matches
+ .get_many::<String>("filter")
+ .unwrap_or_default()
+ .map(String::as_ref)
+ .collect::<Vec<_>>();
let dedup = get_required_arg::<bool>(sub_matches, "dedup")?;
- let output = commands::dump_parsed_flags(input, *format, *dedup)?;
+ let output = commands::dump_parsed_flags(input, format.clone(), &filters, *dedup)?;
let path = get_required_arg::<String>(sub_matches, "out")?;
write_output_to_file_or_stdout(path, &output)?;
}
diff --git a/tools/aconfig/src/storage/mod.rs b/tools/aconfig/src/storage/mod.rs
index 90e05f5..686f9ae 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 = [
(
@@ -108,6 +160,11 @@
"storage_test_2.aconfig",
include_bytes!("../../tests/storage_test_2.aconfig").as_slice(),
),
+ (
+ "com.android.aconfig.storage.test_4",
+ "storage_test_4.aconfig",
+ include_bytes!("../../tests/storage_test_4.aconfig").as_slice(),
+ ),
];
aconfig_files
@@ -143,7 +200,7 @@
}
}
- assert_eq!(packages.len(), 2);
+ assert_eq!(packages.len(), 3);
assert_eq!(packages[0].package_name, "com.android.aconfig.storage.test_1");
assert_eq!(packages[0].package_id, 0);
@@ -162,5 +219,12 @@
assert!(packages[1].flag_names.contains("disabled_ro"));
assert!(packages[1].flag_names.contains("enabled_fixed_ro"));
assert_eq!(packages[1].boolean_offset, 10);
+
+ assert_eq!(packages[2].package_name, "com.android.aconfig.storage.test_4");
+ assert_eq!(packages[2].package_id, 2);
+ assert_eq!(packages[2].flag_names.len(), 2);
+ assert!(packages[2].flag_names.contains("enabled_ro"));
+ assert!(packages[2].flag_names.contains("enabled_fixed_ro"));
+ assert_eq!(packages[2].boolean_offset, 16);
}
}
diff --git a/tools/aconfig/src/storage/package_table.rs b/tools/aconfig/src/storage/package_table.rs
new file mode 100644
index 0000000..940c5b2
--- /dev/null
+++ b/tools/aconfig/src/storage/package_table.rs
@@ -0,0 +1,299 @@
+/*
+ * 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
+ }
+}
+
+#[derive(PartialEq, Debug)]
+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(),
+ };
+
+ // initialize all header fields
+ 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;
+
+ // 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 = table.header.node_offset;
+ 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);
+ }
+ }
+ }
+
+ 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)
+ }
+ }
+
+ impl PackageTable {
+ // test only method to deserialize back into the table struct
+ fn from_bytes(bytes: &[u8]) -> Result<Self> {
+ let header = PackageTableHeader::from_bytes(bytes)?;
+ let num_packages = header.num_packages;
+ let num_buckets = storage::get_table_size(num_packages)?;
+ let mut head = header.as_bytes().len();
+ let buckets = (0..num_buckets)
+ .map(|_| match read_u32_from_bytes(bytes, &mut head).unwrap() {
+ 0 => None,
+ val => Some(val),
+ })
+ .collect();
+ let nodes = (0..num_packages)
+ .map(|_| {
+ let node = PackageTableNode::from_bytes(&bytes[head..], num_buckets).unwrap();
+ head += node.as_bytes().len();
+ node
+ })
+ .collect();
+
+ let table = Self { header, buckets, nodes };
+ Ok(table)
+ }
+ }
+
+ 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: 208,
+ num_packages: 3,
+ 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(58), None, None, Some(108), None, None, None];
+ assert_eq!(buckets, &expected);
+
+ let nodes: &Vec<PackageTableNode> = &package_table.as_ref().unwrap().nodes;
+ assert_eq!(nodes.len(), 3);
+ 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: Some(158),
+ bucket_index: 3,
+ };
+ assert_eq!(nodes[1], second_node_expected);
+ let third_node_expected = PackageTableNode {
+ package_name: String::from("com.android.aconfig.storage.test_4"),
+ package_id: 2,
+ boolean_offset: 16,
+ next_offset: None,
+ bucket_index: 3,
+ };
+ assert_eq!(nodes[2], third_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 package_table = package_table.unwrap();
+
+ let header: &PackageTableHeader = &package_table.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.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());
+ }
+
+ let reinterpreted_table = PackageTable::from_bytes(&package_table.as_bytes());
+ assert!(reinterpreted_table.is_ok());
+ assert_eq!(&package_table, &reinterpreted_table.unwrap());
+ }
+}
diff --git a/tools/aconfig/src/test.rs b/tools/aconfig/src/test.rs
index 71de57e..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"
@@ -225,6 +250,24 @@
}
"#;
+ pub fn parse_read_only_test_flags() -> ProtoParsedFlags {
+ let bytes = crate::commands::parse_flags(
+ "com.android.aconfig.test",
+ Some("system"),
+ vec![Input {
+ source: "tests/read_only_test.aconfig".to_string(),
+ reader: Box::new(include_bytes!("../tests/read_only_test.aconfig").as_slice()),
+ }],
+ vec![Input {
+ source: "tests/read_only_test.values".to_string(),
+ reader: Box::new(include_bytes!("../tests/read_only_test.values").as_slice()),
+ }],
+ crate::commands::DEFAULT_FLAG_PERMISSION,
+ )
+ .unwrap();
+ crate::protos::parsed_flags::try_from_binary_proto(&bytes).unwrap()
+ }
+
pub fn parse_test_flags() -> ProtoParsedFlags {
let bytes = crate::commands::parse_flags(
"com.android.aconfig.test",
diff --git a/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template b/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template
index 8010b88..933d6a7 100644
--- a/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template
+++ b/tools/aconfig/templates/FakeFeatureFlagsImpl.java.template
@@ -12,23 +12,11 @@
}
{{ for item in flag_elements}}
-{{ if library_exported }}
-
-{{ if item.exported }}
@Override
@UnsupportedAppUsage
public boolean {item.method_name}() \{
return getValue(Flags.FLAG_{item.flag_name_constant_suffix});
}
-{{ endif }}
-
-{{ else }}
- @Override
- @UnsupportedAppUsage
- public boolean {item.method_name}() \{
- return getValue(Flags.FLAG_{item.flag_name_constant_suffix});
- }
-{{ endif }}
{{ endfor}}
public void setFlag(String flagName, boolean value) \{
if (!this.mFlagMap.containsKey(flagName)) \{
@@ -52,20 +40,11 @@
}
private Map<String, Boolean> mFlagMap = new HashMap<>(
- {{ if library_exported }}
- Map.ofEntries(
- {{-for item in exported_flag_elements}}
- Map.entry(Flags.FLAG_{item.flag_name_constant_suffix}, false)
- {{ -if not @last }},{{ endif }}
- {{ -endfor }}
- )
- {{ else }}
Map.ofEntries(
{{-for item in flag_elements}}
Map.entry(Flags.FLAG_{item.flag_name_constant_suffix}, false)
{{ -if not @last }},{{ endif }}
{{ -endfor }}
)
- {{ endif }}
);
}
diff --git a/tools/aconfig/templates/FeatureFlags.java.template b/tools/aconfig/templates/FeatureFlags.java.template
index 180f882..5e67b13 100644
--- a/tools/aconfig/templates/FeatureFlags.java.template
+++ b/tools/aconfig/templates/FeatureFlags.java.template
@@ -5,15 +5,10 @@
/** @hide */
public interface FeatureFlags \{
{{ for item in flag_elements }}
-{{ if library_exported }}
-
-{{ if item.exported }}
+{{ -if library_exported }}
@UnsupportedAppUsage
boolean {item.method_name}();
-{{ endif }}
-
-{{ else }}
-
+{{ -else }}
{{ -if not item.is_read_write }}
{{ -if item.default_value }}
@com.android.aconfig.annotations.AssumeTrueForR8
@@ -23,7 +18,6 @@
{{ endif }}
@UnsupportedAppUsage
boolean {item.method_name}();
-
{{ endif }}
-{{ endfor }}
+{{ -endfor }}
}
diff --git a/tools/aconfig/templates/FeatureFlagsImpl.java.template b/tools/aconfig/templates/FeatureFlagsImpl.java.template
index 7a52ceb..28baa41 100644
--- a/tools/aconfig/templates/FeatureFlagsImpl.java.template
+++ b/tools/aconfig/templates/FeatureFlagsImpl.java.template
@@ -1,54 +1,42 @@
package {package_name};
// TODO(b/303773055): Remove the annotation after access issue is resolved.
import android.compat.annotation.UnsupportedAppUsage;
-{{ if not is_test_mode }}
-{{ if runtime_lookup_required- }}
+{{ -if not is_test_mode }}
+{{ -if runtime_lookup_required }}
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
{{ endif }}
/** @hide */
public final class FeatureFlagsImpl implements FeatureFlags \{
-{{- if runtime_lookup_required }}
-{{- for namespace_with_flags in namespace_flags }}
+{{ -if runtime_lookup_required }}
+{{ -for namespace_with_flags in namespace_flags }}
private static boolean {namespace_with_flags.namespace}_is_cached = false;
-{{- endfor- }}
+{{ -endfor- }}
{{ for flag in flag_elements }}
-{{ if library_exported }}
-{{ if flag.exported }}
+{{ -if library_exported }}
private static boolean {flag.method_name} = false;
-{{ endif }}
-
-{{ else }}
-
+{{ -else }}
{{- if flag.is_read_write }}
private static boolean {flag.method_name} = {flag.default_value};
{{- endif- }}
-{{ endif }}
-{{ endfor }}
-
+{{ -endif }}
+{{ -endfor }}
{{ for namespace_with_flags in namespace_flags }}
private void load_overrides_{namespace_with_flags.namespace}() \{
try \{
Properties properties = DeviceConfig.getProperties("{namespace_with_flags.namespace}");
-
- {{- for flag in namespace_with_flags.flags }}
- {{ if library_exported }}
-
- {{ if flag.exported }}
+{{ -for flag in namespace_with_flags.flags }}
+{{ -if library_exported }}
{flag.method_name} =
properties.getBoolean("{flag.device_config_flag}", false);
- {{ endif }}
-
- {{ else }}
-
- {{ if flag.is_read_write }}
+{{ -else }}
+{{ -if flag.is_read_write }}
{flag.method_name} =
properties.getBoolean("{flag.device_config_flag}", {flag.default_value});
- {{ endif }}
-
- {{ endif }}
- {{ endfor }}
+{{ -endif }}
+{{ -endif }}
+{{ -endfor }}
} catch (NullPointerException e) \{
throw new RuntimeException(
"Cannot read value from namespace {namespace_with_flags.namespace} "
@@ -62,37 +50,27 @@
{namespace_with_flags.namespace}_is_cached = true;
}
{{ endfor- }}
-{{ endif- }}
-
-{{ for flag in flag_elements }}
-{{ if library_exported }}
-
-{{ if flag.exported }}
+{{ -endif }}{#- end of runtime_lookup_required #}
+{{ -for flag in flag_elements }}
@Override
@UnsupportedAppUsage
public boolean {flag.method_name}() \{
+{{ -if library_exported }}
if (!{flag.device_config_namespace}_is_cached) \{
load_overrides_{flag.device_config_namespace}();
}
return {flag.method_name};
- }
-{{ endif }}
-
-{{ else }}
- @Override
- @UnsupportedAppUsage
- public boolean {flag.method_name}() \{
- {{ -if flag.is_read_write }}
+{{ -else }}
+{{ -if flag.is_read_write }}
if (!{flag.device_config_namespace}_is_cached) \{
load_overrides_{flag.device_config_namespace}();
}
return {flag.method_name};
- {{ else }}
+{{ -else }}
return {flag.default_value};
- {{ endif- }}
+{{ -endif- }}
+{{ -endif }}
}
-{{ endif }}
-
{{ endfor }}
}
{{ else }}
diff --git a/tools/aconfig/templates/Flags.java.template b/tools/aconfig/templates/Flags.java.template
index 9f4c52f..34b8189 100644
--- a/tools/aconfig/templates/Flags.java.template
+++ b/tools/aconfig/templates/Flags.java.template
@@ -5,42 +5,30 @@
/** @hide */
public final class Flags \{
-{{- for item in flag_elements}}
- {{ if library_exported }}
- {{ if item.exported }}
+{{ -for item in flag_elements}}
/** @hide */
public static final String FLAG_{item.flag_name_constant_suffix} = "{item.device_config_flag}";
- {{ endif }}
- {{ else }}
- /** @hide */
- public static final String FLAG_{item.flag_name_constant_suffix} = "{item.device_config_flag}";
- {{ endif }}
{{- endfor }}
-{{ for item in flag_elements}}
+{{ -for item in flag_elements}}
{{ if library_exported }}
-
-{{ if item.exported }}
@UnsupportedAppUsage
public static boolean {item.method_name}() \{
return FEATURE_FLAGS.{item.method_name}();
}
-{{ endif }}
-
-{{ else }}
-
+{{ -else }}
{{ -if not item.is_read_write }}
{{ -if item.default_value }}
@com.android.aconfig.annotations.AssumeTrueForR8
{{ -else }}
@com.android.aconfig.annotations.AssumeFalseForR8
-{{ -endif- }}
-{{ endif }}
+{{ -endif }}
+{{ -endif }}
@UnsupportedAppUsage
public static boolean {item.method_name}() \{
return FEATURE_FLAGS.{item.method_name}();
}
-{{ endif }}
-{{ endfor }}
+{{ -endif }}
+{{ -endfor }}
{{ -if is_test_mode }}
public static void setFeatureFlags(FeatureFlags featureFlags) \{
Flags.FEATURE_FLAGS = featureFlags;
@@ -49,7 +37,8 @@
public static void unsetFeatureFlags() \{
Flags.FEATURE_FLAGS = null;
}
-{{ endif }}
+{{ -endif }}
+
private static FeatureFlags FEATURE_FLAGS{{ -if not is_test_mode }} = new FeatureFlagsImpl(){{ -endif- }};
}
diff --git a/tools/aconfig/templates/cpp_exported_header.template b/tools/aconfig/templates/cpp_exported_header.template
index cc1b18d..6b6daa7 100644
--- a/tools/aconfig/templates/cpp_exported_header.template
+++ b/tools/aconfig/templates/cpp_exported_header.template
@@ -1,16 +1,16 @@
#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
#endif
-{{ for item in class_elements- }}
-{{ if item.is_fixed_read_only- }}
+{{ for item in class_elements }}
+{{ -if item.is_fixed_read_only }}
#ifndef {package_macro}_{item.flag_macro}
#define {package_macro}_{item.flag_macro} {item.default_value}
#endif
-{{ endif }}
+{{ -endif }}
{{ -endfor }}
{{ -endif }}
{{ -endif }}
@@ -24,15 +24,15 @@
class flag_provider_interface \{
public:
virtual ~flag_provider_interface() = default;
- {{ for item in class_elements}}
+ {{ -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 }}
};
@@ -41,29 +41,35 @@
{{ for item in class_elements}}
inline bool {item.flag_name}() \{
- {{ if for_test }}
+ {{ -if is_test_mode }}
return provider_->{item.flag_name}();
- {{ -else- }}
- {{ if item.readwrite- }}
+ {{ -else }}
+ {{ -if is_prod_mode }}
+ {{ -if item.readwrite }}
return provider_->{item.flag_name}();
- {{ -else- }}
- {{ if item.is_fixed_read_only }}
+ {{ -else }}
+ {{ -if item.is_fixed_read_only }}
return {package_macro}_{item.flag_macro};
- {{ -else- }}
+ {{ -else }}
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();
}
@@ -77,12 +83,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 1bfa4b6..4aec540 100644
--- a/tools/aconfig/templates/cpp_source_file.template
+++ b/tools/aconfig/templates/cpp_source_file.template
@@ -1,17 +1,20 @@
#include "{header}.h"
-{{ if readwrite }}
+
+{{ if readwrite- }}
#include <server_configurable_flags/get_flags.h>
{{ endif }}
-{{ if for_test }}
+{{ if is_test_mode }}
#include <unordered_map>
#include <string>
{{ -else- }}
+{{ if readwrite- }}
#include <vector>
-{{ endif }}
+{{ -endif }}
+{{ -endif }}
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_;
@@ -21,7 +24,7 @@
: overrides_()
\{}
- {{ for item in class_elements}}
+{{ for item in class_elements }}
virtual bool {item.flag_name}() override \{
auto it = overrides_.find("{item.flag_name}");
if (it != overrides_.end()) \{
@@ -32,7 +35,7 @@
"aconfig_flags.{item.device_config_namespace}",
"{item.device_config_flag}",
"{item.default_value}") == "true";
- {{ -else- }}
+ {{ -else }}
return {item.default_value};
{{ -endif }}
}
@@ -41,7 +44,7 @@
virtual void {item.flag_name}(bool val) override \{
overrides_["{item.flag_name}"] = val;
}
- {{ endfor }}
+{{ endfor }}
virtual void reset_flags() override \{
overrides_.clear();
@@ -52,9 +55,12 @@
class flag_provider : public flag_provider_interface \{
public:
- {{ for item in class_elements}}
+
+ {{ -for item in class_elements }}
+
virtual bool {item.flag_name}() override \{
- {{ if item.readwrite- }}
+ {{ -if is_prod_mode }}
+ {{ -if item.readwrite }}
if (cache_[{item.readwrite_idx}] == -1) \{
cache_[{item.readwrite_idx}] = server_configurable_flags::GetServerConfigurableFlag(
"aconfig_flags.{item.device_config_namespace}",
@@ -62,17 +68,30 @@
"{item.default_value}") == "true";
}
return cache_[{item.readwrite_idx}];
- {{ -else- }}
- {{ if item.is_fixed_read_only }}
+ {{ -else }}
+ {{ -if item.is_fixed_read_only }}
return {package_macro}_{item.flag_macro};
- {{ -else- }}
+ {{ -else }}
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 }}
+ {{ -endfor }}
+ {{ if readwrite- }}
private:
std::vector<int8_t> cache_ = std::vector<int8_t>({readwrite_count}, -1);
+ {{ -endif }}
};
@@ -82,32 +101,37 @@
std::make_unique<flag_provider>();
}
-
-{{ for item in class_elements}}
+{{ 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 item.readwrite- }}
+ {{ -else }}
+ {{ -if is_prod_mode }}
+ {{ -if item.readwrite }}
return {cpp_namespace}::{item.flag_name}();
- {{ -else- }}
- {{ if item.is_fixed_read_only }}
+ {{ -else }}
+ {{ -if item.is_fixed_read_only }}
return {package_macro}_{item.flag_macro};
- {{ -else- }}
+ {{ -else }}
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 -}}
+{{ 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..110f2d4
--- /dev/null
+++ b/tools/aconfig/templates/rust_exported.template
@@ -0,0 +1,35 @@
+//! 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/templates/rust_prod.template b/tools/aconfig/templates/rust_prod.template
index 30ea646..f9a2829 100644
--- a/tools/aconfig/templates/rust_prod.template
+++ b/tools/aconfig/templates/rust_prod.template
@@ -3,32 +3,32 @@
/// flag provider
pub struct FlagProvider;
-{{ if has_readwrite - }}
+{{ if has_readwrite- }}
lazy_static::lazy_static! \{
- {{ for flag in template_flags }}
- {{ if flag.readwrite -}}
+{{ -for flag in template_flags }}
+ {{ -if flag.readwrite }}
/// 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}",
"{flag.default_value}") == "true";
{{ -endif }}
- {{ endfor }}
+{{ -endfor }}
}
{{ -endif }}
impl FlagProvider \{
- {{ for flag in template_flags }}
+{{ for flag in template_flags }}
/// query flag {flag.name}
pub fn {flag.name}(&self) -> bool \{
- {{ if flag.readwrite -}}
+ {{ -if flag.readwrite }}
*CACHED_{flag.name}
- {{ -else- }}
+ {{ -else }}
{flag.default_value}
{{ -endif }}
}
- {{ endfor }}
+{{ endfor }}
}
@@ -38,10 +38,10 @@
{{ for flag in template_flags }}
/// query flag {flag.name}
#[inline(always)]
-{{ if flag.readwrite -}}
+{{ -if flag.readwrite }}
pub fn {flag.name}() -> bool \{
PROVIDER.{flag.name}()
-{{ -else- }}
+{{ -else }}
pub fn {flag.name}() -> bool \{
{flag.default_value}
{{ -endif }}
diff --git a/tools/aconfig/templates/rust_test.template b/tools/aconfig/templates/rust_test.template
index fd1229b..d01f40a 100644
--- a/tools/aconfig/templates/rust_test.template
+++ b/tools/aconfig/templates/rust_test.template
@@ -9,7 +9,7 @@
}
impl FlagProvider \{
- {{ for flag in template_flags }}
+{{ for flag in template_flags }}
/// query flag {flag.name}
pub fn {flag.name}(&self) -> bool \{
self.overrides.get("{flag.name}").copied().unwrap_or(
@@ -28,7 +28,7 @@
pub fn set_{flag.name}(&mut self, val: bool) \{
self.overrides.insert("{flag.name}", val);
}
- {{ endfor }}
+{{ endfor }}
/// clear all flag overrides
pub fn reset_flags(&mut self) \{
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/read_only_test.aconfig b/tools/aconfig/tests/read_only_test.aconfig
new file mode 100644
index 0000000..5eb5056
--- /dev/null
+++ b/tools/aconfig/tests/read_only_test.aconfig
@@ -0,0 +1,32 @@
+package: "com.android.aconfig.test"
+container: "system"
+
+flag {
+ name: "enabled_ro"
+ namespace: "aconfig_test"
+ description: "This flag is ENABLED + READ_ONLY"
+ bug: "abc"
+}
+
+flag {
+ name: "disabled_ro"
+ namespace: "aconfig_test"
+ description: "This flag is DISABLED + READ_ONLY"
+ bug: "123"
+}
+
+flag {
+ name: "enabled_fixed_ro"
+ namespace: "aconfig_test"
+ description: "This flag is fixed READ_ONLY + ENABLED"
+ bug: ""
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "disabled_fixed_ro"
+ namespace: "aconfig_test"
+ description: "This flag is fixed READ_ONLY + DISABLED"
+ bug: ""
+ is_fixed_read_only: true
+}
diff --git a/tools/aconfig/tests/read_only_test.values b/tools/aconfig/tests/read_only_test.values
new file mode 100644
index 0000000..349c7aa
--- /dev/null
+++ b/tools/aconfig/tests/read_only_test.values
@@ -0,0 +1,18 @@
+flag_value {
+ package: "com.android.aconfig.test"
+ name: "disabled_ro"
+ state: DISABLED
+ permission: READ_ONLY
+}
+flag_value {
+ package: "com.android.aconfig.test"
+ name: "enabled_ro"
+ state: ENABLED
+ permission: READ_ONLY
+}
+flag_value {
+ package: "com.android.aconfig.test"
+ name: "enabled_fixed_ro"
+ state: ENABLED
+ permission: READ_ONLY
+}
diff --git a/tools/aconfig/tests/storage_test_4.aconfig b/tools/aconfig/tests/storage_test_4.aconfig
new file mode 100644
index 0000000..333fe09
--- /dev/null
+++ b/tools/aconfig/tests/storage_test_4.aconfig
@@ -0,0 +1,17 @@
+package: "com.android.aconfig.storage.test_4"
+container: "system"
+
+flag {
+ name: "enabled_ro"
+ namespace: "aconfig_test"
+ description: "This flag is ENABLED + READ_ONLY"
+ bug: "abc"
+}
+
+flag {
+ name: "enabled_fixed_ro"
+ namespace: "aconfig_test"
+ description: "This flag is fixed READ_ONLY + ENABLED"
+ bug: ""
+ is_fixed_read_only: true
+}
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/compliance/go.mod b/tools/compliance/go.mod
index 1928189..bd04077 100644
--- a/tools/compliance/go.mod
+++ b/tools/compliance/go.mod
@@ -26,4 +26,4 @@
// Indirect dep from go-cmp
exclude golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
-go 1.18
+go 1.21
diff --git a/tools/perf/benchmarks b/tools/perf/benchmarks
new file mode 100755
index 0000000..f46b920
--- /dev/null
+++ b/tools/perf/benchmarks
@@ -0,0 +1,709 @@
+#!/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 random
+import re
+import shutil
+import subprocess
+import time
+import uuid
+
+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 Create(filename):
+ "Create an action to create `filename`. The parent directory must exist."
+ def create():
+ with open(filename, "w") as f:
+ pass
+ def delete():
+ os.remove(filename)
+ return Change(
+ label=f"Create {filename}",
+ change=create,
+ undo=delete,
+ )
+
+
+def Modify(filename, contents, before=None):
+ """Create an action to modify `filename` by appending the result of `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:])
+ if False:
+ print(f"Modify: {filename}")
+ x = orig.contents.replace("\n", "\n ORIG")
+ print(f" ORIG {x}")
+ x = modified.contents.replace("\n", "\n MODIFIED")
+ print(f" MODIFIED {x}")
+
+ return Change(
+ label="Modify " + filename,
+ change=lambda: modified.write(),
+ undo=lambda: orig.write()
+ )
+
+def AddJavaField(filename, prefix):
+ return Modify(filename,
+ lambda: f"{prefix} static final int BENCHMARK = {random.randint(0, 1000000)};\n",
+ before="}")
+
+
+def Comment(prefix, suffix=""):
+ return lambda: prefix + " " + str(uuid.uuid4()) + suffix
+
+
+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
+
+ dist_one = self._options.DistOne()
+ if dist_one:
+ # If we're disting just one benchmark, save the logs and we can stop here.
+ self._dist(dist_one)
+ else:
+ # 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 _dist(self, dist_dir):
+ out_dir = pathlib.Path("out")
+ dest_dir = pathlib.Path(dist_dir).joinpath("logs")
+ os.makedirs(dest_dir, exist_ok=True)
+ basenames = [
+ "build.trace.gz",
+ "soong.log",
+ "soong_build_metrics.pb",
+ "soong_metrics",
+ ]
+ for base in basenames:
+ src = out_dir.joinpath(base)
+ if src.exists():
+ sys.stderr.write(f"DIST: copied {src} to {dest_dir}\n")
+ shutil.copy(src, dest_dir)
+
+ 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.")
+ parser.add_argument("--dist-one", type=str,
+ help="Copy logs and metrics to the given dist dir. Requires that only"
+ + " one benchmark be supplied. Postroll steps will be skipped.")
+
+ 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}")
+
+ # --dist-one requires that only one benchmark be supplied
+ if len(self.Benchmarks()) != 1:
+ self._error("--dist-one requires that exactly one --benchmark.")
+
+ 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 DistOne(self):
+ return self._args.dist_one
+
+ 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="unreferenced",
+ title="Create unreferenced file",
+ change=Create("bionic/unreferenced.txt"),
+ modules=["droid"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="modify_bp",
+ title="Modify Android.bp",
+ change=Modify("bionic/libc/Android.bp", Comment("//")),
+ modules=["droid"],
+ preroll=1,
+ postroll=3,
+ ),
+ Benchmark(id="modify_stdio",
+ title="Modify stdio.cpp",
+ change=Modify("bionic/libc/stdio/stdio.cpp", Comment("//")),
+ modules=["libc"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="modify_adbd",
+ title="Modify adbd",
+ change=Modify("packages/modules/adb/daemon/main.cpp", Comment("//")),
+ modules=["adbd"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="services_private_field",
+ title="Add private field to ActivityManagerService.java",
+ change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
+ "private"),
+ modules=["services"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="services_public_field",
+ title="Add public field to ActivityManagerService.java",
+ change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
+ "/** @hide */ public"),
+ modules=["services"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="services_api",
+ title="Add API to ActivityManagerService.javaa",
+ change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
+ "@android.annotation.SuppressLint(\"UnflaggedApi\") public"),
+ modules=["services"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="framework_private_field",
+ title="Add private field to Settings.java",
+ change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
+ "private"),
+ modules=["framework-minus-apex"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="framework_public_field",
+ title="Add public field to Settings.java",
+ change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
+ "/** @hide */ public"),
+ modules=["framework-minus-apex"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="framework_api",
+ title="Add API to Settings.java",
+ change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
+ "@android.annotation.SuppressLint(\"UnflaggedApi\") public"),
+ modules=["framework-minus-apex"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="modify_framework_resource",
+ title="Modify framework resource",
+ change=Modify("frameworks/base/core/res/res/values/config.xml",
+ lambda: str(uuid.uuid4()),
+ before="</string>"),
+ modules=["framework-minus-apex"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="add_framework_resource",
+ title="Add framework resource",
+ change=Modify("frameworks/base/core/res/res/values/config.xml",
+ lambda: f"<string name=\"BENCHMARK\">{uuid.uuid4()}</string>",
+ before="</resources>"),
+ modules=["framework-minus-apex"],
+ preroll=1,
+ postroll=2,
+ ),
+ Benchmark(id="add_systemui_field",
+ title="Add SystemUI field",
+ change=AddJavaField("frameworks/base/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java",
+ "public"),
+ modules=["SystemUI"],
+ preroll=1,
+ postroll=2,
+ ),
+ ]
+
+ 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
diff --git a/tools/rbcrun/go.mod b/tools/rbcrun/go.mod
index 5ae2972..6e99ce9 100644
--- a/tools/rbcrun/go.mod
+++ b/tools/rbcrun/go.mod
@@ -4,4 +4,4 @@
replace go.starlark.net => ../../../../external/starlark-go
-go 1.15
+go 1.21