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