深入浅出 Gradle Sync 优化

动手点关注 干货不迷路 👆

本文分析了 Android Studio Sync 在 Gradle 层面的底层逻辑,并且从原理出发介绍了 DevOps - Build 团队 Gradle Sync 优化框架的实现细节以及在飞书项目中进行 Sync 优化的实战经验。

高频却不高效的 Sync

作为 Android 开发者,我们在使用 Android Studio 时,离不开名为 Sync 的操作:代码索引、自动补全等功能均需通过成功的 Sync 过程方可使用。以飞书工程 2021 年 10 月为例,一个月内共触发 Sync 3805 次,日均触发 172.95 次。

ba008e0186135a261b872912107cf405.png

然而这样一个日常开发中的高频操作其平均耗时近 3min,远超同期飞书增量编译的平均耗时(1.77min)。如果能够将 Sync 操作的耗时缩短,显然能够大幅提升工程师的开发效率。

Sync 过程探秘

若要优化 Sync 过程,首先要理解在 Android Studio 中点击 Gradle Sync 按钮后,都发生了什么。

从用户视角看,项目的全部信息均存储于工程的代码中,而 Android Studio 需要做的事情便是将这些信息转化为 GUI 层面可视元素。由于涉及到获取工程源代码和依赖等信息,最直接的想法便是通过构建系统来获得上述信息,而目前 Android 工程普遍使用的构建系统 Gradle 恰巧提供了该能力,那便是 Tooling API。

Tooling API:IDE 与 Gradle 沟通的桥梁

Gradle 提供了名为 Tooling API 的编程 API,开发者可以通过它来将 Gradle 嵌入自己的软件中。使用该 API 可以触发并监听构建,还可以查询构建中的运行时信息。

以 Android Studio 为例,用户在 Android Studio 中点击了 Sync 按钮后,Android Studio 便调用 Tooling API 触发了一次 Gradle 构建过程,并通过 Tooling API 获取 Gradle 构建过程中可以拿到的详细信息(例如 Gradle 版本、模块名称、依赖等等),Tooling API 成为了 Android Studio 和 Gradle 两个 Java 进程之间的桥梁。

Android Studio 与 Gradle 之间的通信类似于常见的 Client - Server 模型:

  • Android Studio 作为 Client 发起请求,传递参数告知 Server 自己需要哪些数据

  • Gradle 进程作为 Server,通过一次构建过程收集 Client 所需的信息并回传

  • Client 和 Server 两个进程间通过 Socket 进行通信,Tooling API 则是提供给 Client 端调用的对于通信过程的封装

IDE 与 Gradle 的通信协议:Tooling Model

既然涉及到两个进程之间的通信,便需要一套能够使双方能够相互理解的协议,在 Gradle 中该协议称之为 Tooling Model。

为了方便大家理解,这里使用一个示例工程来展示上述进程间通信。该示例工程是一个通过 Tooling API 获取目标 Gradle 项目中 Gradle Plugin 插件名称并打印的 Java 应用。

约定数据格式

由于 Client 与 Server 均为 Java 应用,我们可以直接通过接口来定义二者之间的数据格式,接口类对于 Client 与 Server 均可见,而接口的实现类则可以仅对实际生产数据的 Server 端可见即可,这也符合面向接口编程的设计理念。

在这里,作为数据格式接口类如下:

public interface GradlePluginModel {
    // 目标工程的 Gradle Plugin 名称列表
    List<String> getPlugins();
}

由于涉及在进程间传递,实际传递的实现类的对象需要进行序列化与反序列化,所以该接口的实现类也实现了 java.io.Serializable 接口:

public class GradlePluginModelImpl implements GradlePluginModel, Serializable {
    public static final long serialVersionUID = 42L;

    private List<String> plugins;

    public GradlePluginModelImpl(List<String> plugins) {
        this.plugins = plugins;
    }

    @Override
    public List<String> getPlugins() {
        return plugins;
    }
}
编写数据获取逻辑

实际获取数据的逻辑运行于 Gradle 进程中,需要以 Gradle 插件的形式应用于目标 Gradle 工程中。

在示例工程中,为了简化流程,进行数据获取的 Gradle 插件直接在目标工程的buildSrc 中定义并在 build.gradle 中进行应用:

apply plugin: GradlePlugin
...

实际上,Client 端也可以通过 init script 将自定义的 Gradle 插件注入目标构建中来自定义数据获取的逻辑,Android Studio 中代表 sources.jar 的 Tooling Model AdditionalClassifierArtifactsModel 的获取过程便是通过该方式注入的,而 Android 以及 Kotlin 等 Tooling Model 的获取则是通过实际应用于项目的 Android Gradle Plugin 和 Kotlin Gradle Plugin 获取的。

在 Gradle 插件中定义 Tooling Model 获取逻辑,需要实现 ToolingModelBuilder 接口:

public class GradlePluginModelBuilder implements ToolingModelBuilder {
    @Override
    public boolean canBuild(@Nonnull String modelName) {
        System.out.println("[modelName]" + modelName);
        return Objects.equals(modelName, GradlePluginModel.class.getName());
    }

    @Override
    @Nullable
    public Object buildAll(@Nonnull String modelName, @Nonnull Project project) {
        List<String> plugins = new ArrayList<>();
        project.getPlugins().forEach(plugin -> plugins.add(project.getPath() + " -> " + plugin.getClass().getName()));
        return new GradlePluginModelImpl(plugins);
    }
}

其中 canBuild 方法用以判断 Client 端的请求是否应由该 Builder 响应,入参的 modelName 即目标 Tooling Model 接口的完整类名;buildAll 方法为实际获取数据的逻辑,该方法返回前文所述的 Tooling Model 实现类的对象。示例工程中的逻辑较为简单,即获取入参 Project的所有插件的类名信息。

ToolingModelBuilder需要通过 ToolingModelBuilderRegistry 在 Gradle 插件中进行注册:

public class GradlePlugin implements Plugin<Project> {

    private final ToolingModelBuilderRegistry registry;

    @Inject
    public GradlePlugin(ToolingModelBuilderRegistry registry) {
        this.registry = registry;
    }


    @Override
    public void apply(@Nonnull Project project) {
        registry.register(new GradlePluginModelBuilder());
    }
}
Client 端通过 Tooling API 发起请求

在 Client 端,可以很简单地使用 Tooling API 获取前文定义的 Tooling Model:

public class Main {
    public static void main(String[] args) {
        GradleConnector connector = GradleConnector.newConnector();
        File projectDir = new File("../app");
        connector.forProjectDirectory(projectDir);
        try (ProjectConnection connection = connector.connect()) {
            GradlePluginModel model = connection.getModel(GradlePluginModel.class);
            println("***************************************");
            println("Fetch model: ");
            model.getPlugins().forEach(Main::println);
            println("***************************************");
        }
    }

    private static void println(String msg) {
        System.out.println("[tooling] " + msg);
    }
}

上述逻辑可以简化为下图的流程:

671da711affe52cb20bd716560bb4404.png
Android Studio 中的 Tooling API 调用

Android Studio 的 Sync 过程需要各种各样的数据,但本质上都与示例工程的 Tooling Model 获取逻辑一致,二者之间的区别在于:

  • Android Studio 除了会获取直接在目标工程 build.gradle 中应用的 Gradle 插件中注册的 Tooling Model 外,还会通过在调用 Tooling API 时传入 init script 注入自定义的用于获取 Tooling Model 的 Gradle 插件

  • Android Studio 的 Sync 是通过 BuildAction 接口在一次 Tooling API 调用中获取了多个 Tooling Model 而非示例工程中通过 getModel 方法获取单个 Tooling Model。Android Studio 中的 Tooling API 调用位于 GradleProjectResolver#doResolveProjectInfo,BuildAction 的实现则位于 ProjectImportAction,由于只是 API 调用差异而核心逻辑一致,故在此不予赘述,感兴趣的读者可以至上述源码处查看详细实现

  • Android Studio 底层依托于 IntelliJ IDEA Platform SDK,后者提供了名为 GradleProjectResolverExtension 的扩展点,IDE 插件可以通过该扩展定义自己的 Tooling Model 构建逻辑,这里的 ToolingModelBuilder 最终会通过上文提到的 BuildAction 注入 Sync 过程触发的 Gradle 构建中,所以我们可以通过在自定义插件中定义该扩展来在 Sync 时实现自定义的数据获取与展示逻辑。

关于 Intellij IDEA Platform SDK 中的扩展概念,可以参考文档:Extensions | Intellij Platform(https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html)

Sync 时的 Gradle 构建

那么 Sync 时 Gradle 究竟做了哪些事情呢?为了方便大家理解,我们以飞书工程的一次完整 Sync 过程为例,用 Chrome Trace 的形式可视化其对应 Gradle 构建过程中的所有 BuildOperation:

d6c3def7eac17f756340d38abef000c9.png

在 Gradle 构建中,可以通过传入参数 -Dorg.gradle.internal.operations.trace 获取上图中的 Chrome Trace,对于 Android Studio 的 Gradle Sync,我们可以通过断点 Android Studio 的 Tooling API 调用处,传入该参数获取 Sync 时 BuildOperation 的 Chrome Trace。

一次常规 Gradle 构建包含三个阶段:

  • Initialization:该阶段中 Gradle 确定哪些 Project 将参与构建并为其创建对应的 Project 实例

  • Configuration:该阶段中 Gradle 对参与构建的 Project 对象进行配置,执行这些 Project 对象的构建脚本

  • Execution:Gradle 判断需要执行的 task 子集并最终执行选中的 task

参考原文:Gradle Document: Build LifeCycle(https://docs.gradle.org/current/userguide/build_lifecycle.html#sec:build_phases)

从前文 Chrome Trace 可以看到,由 Sync 触发的 Gradle 构建也基本符合这三个阶段的划分,只是在 Execution 阶段 Sync 触发的构建在串行构建各类 Tooling Model。

Sync 优化实战

了解了 Sync 过程中 Gradle 构建做的事情,我们便可以从这三个阶段分别入手,优化 Sync 操作。

Initialization 和 Configuration 阶段的优化

对于这两个阶段的优化思路较为一致,故合并为一节讲解。

在这两个阶段中,主要在解析与执行工程中的构建脚本与插件,我们可以使用 async profiler 查看这两个过程中的火焰图,定位到耗时脚本/插件,而后根据其与 Sync 的关系分别处理:

  • 对于 Sync 过程中无需执行的,直接判断当前是否为 Sync 触发的构建,若是直接跳过该脚本或插件的执行

  • 对于 Sync 过程中需要执行的,则根据火焰图定位其耗时函数,优化代码实现

跳过脚本或插件执行

我们可以通过如下代码判断当前构建是否为 Sync:

ext.isSync = Objects.equals(gradle.startParameter.projectProperties.get("android.injected.build.model.only"), "true");

而后,使用该标志对 Sync 时无需应用的插件和脚本进行跳过:

if (!isSync) {
    apply plugin: "org.gradle.android.cache-fix"
}
优化代码实现

这里举飞书工程 Sync 优化过程中的典型例子,方便读者理解。

使用 configureEach 替代 all 或 each

一个常见的会导致 Configuration 阶段性能劣化的实现便是对于 Gradle 中的集合类调用 all 或 each 方法进行遍历等操作:

gradle.taskGraph.whenReady {
    tasks.each { task ->
        if (!task.name.contains("mergeExtDex")) {
          return
        }
        task.outputs.cacheIf { false }
    }
}

上述代码会导致添加至 taskGraph 的所有 task 均被创建,从而拖慢配置过程。可以通过将其替换为 configureEach 来规避该劣化行为:

gradle.taskGraph.whenReady {
    tasks.configureEach { task ->
        if (!task.name.contains("mergeExtDex")) {
          return
        }
        task.outputs.cacheIf { false }
    }
}

Execution 阶段优化:BuildInfra Gradle Sync 优化方案

由 DevOps - Build 团队推出的 Gradle Sync 优化方案 重点在于优化占据 Sync 过程最大比例的 Execution 阶段。该方案在飞书工程上收获了近 50% 的收益,本节将着重讲解该方案底层的优化原理。

Tooling Model 缓存优化

如前文所述,Gradle Sync 的目的便是通过构建各种各样的 Tooling Model 实现 Android Studio 所需的数据展示。这些 Tooling Model 代表的数据大多数与工程的构建环境相关(例如 Gradle 版本、Kotlin 版本、模块路径、模块的 sourceSet 等),而这些配置在工程中变更的频率较低,但每次 Sync 过程却都要重新执行一次这些 Tooling Model 的构建逻辑,显然是某种程度上的时间浪费。我们可以通过将 Tooling Model 缓存起来,然后在下次 Sync 时直接返回缓存来达到加速的目的。

虽然缓存是性能优化的常规操作,但也往往会带来错误。对于 Tooling Model 也是如此。这里我们要小心处理:

  1. 并非所有 Tooling Model 均可缓存。上文所述的配置相关的、不常变更的 Tooling Model 固然可以缓存,但一些 Tooling Model,如下文提到的用于文件下载的 Tooling Model AdditionalClassifierArtifactsModel,明显是不可缓存的(新增依赖而不进行 sources.jar 下载会导致在 Android Studio 中无法查看这部分依赖的源码)。我们需要仔细确认每个 Tooling Model 是否可缓存

  2. 构建环境虽然不常变更,但为了提升功能的准确性,显然需要有缓存的“淘汰”机制,防止过期的缓存导致错误

内存 - 磁盘二级缓存

Gradle 中构建 Tooling Model 的逻辑位于如下位置:

// DefaultBuildController.java
@Override
public BuildResult<?> getModel(Object target, ModelIdentifier modelIdentifier, Object parameter)
    throws BuildExceptionVersion1, InternalUnsupportedModelException {
    ...
    Object model;
    if (parameter == null) {
        model = builder.buildAll(modelName, project);
    } else if (builder instanceof ParameterizedToolingModelBuilder<?>) {
        model = getParameterizedModel(project, modelName, (ParameterizedToolingModelBuilder<?>) builder, parameter);
    } else {
        throw (InternalUnsupportedModelException) (new InternalUnsupportedModelException()).initCause(
            new UnknownModelException(String.format("No parameterized builders are available to build a model of type '%s'.", modelName)));
    }

    return new ProviderBuildResult<Object>(model);
}

我们可以通过在该方法中嵌入缓存逻辑,从而实现加速的目的。

Sync 优化框架中,设计了内存 - 磁盘二级缓存,以保证读取缓存效率的同时提升缓存命中率,并通过自定义的 BuildController —— CacheableBuildController 替换默认的 BuildController 来嵌入缓存逻辑。

897854f0d1af8766b14f1d634e8172ac.png

缓存读取与写入时,需要为每一个 Tooling Model 生成一个 key 值。除了 Tooling Model 的名称(即其接口的完整类名)外,部分 Tooling Model 与 Project 对象关联,生成 key 时需要加入 Project 的 path 来进行区别。

内存缓存是使用一个简单的 Map 结构实现的,这里不予赘述。

对于磁盘缓存,Tooling Model 均为实现了 Serializable 接口的 Java Bean,我们可以十分方便的使用 Java Object Stream 将其序列化至磁盘文件:

// DiskBasedBuildModelCache.java
public static boolean put(Project project, String modelName, Object model) {
    ...
    try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
        try (ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(model);
            Logger.info("Write model " + project.getDisplayName() + ":" + modelName + " to disk succeeded");
            return true;
        }
    } catch (Throwable e) {
        ...
    }
}

但在反序列化时,由于:

  • Tooling Model 的实现类位于对应的 Gradle Plugin 中,并由对应 Gradle Plugin 的 ClassLoader 加载

  • BuildController 以及缓存管理类位于 Gradle 框架层,由框架层 ClassLoader 加载

直接进行反序列化会直接抛出 ClassNotFoundException。

于是,为了成功反序列化,Sync 优化框架将 Tooling Model 对象写入磁盘的同时,也将该对象的类的 classpath 写入了磁盘配置文件中:

// DiskBasedBuildModelCache.java
private static void addClassJarPath(Class<?> clazz) throws UnsupportedEncodingException {
    String path = clazz.getProtectionDomain().getCodeSource().getLocation().getPath();
    String decodePath = URLDecoder.decode(path, "UTF-8");
    jarPaths.add(decodePath);
    ...
}

在反序列化时,则使用自定义的 ObjectInputStream ,使用添加了配置文件中 classpath 路径的自定义 ClassLoader 进行类加载:

// 生成自定义的 ClassLoader
public class SyncModelJarClassLoaderFactory {
    public static ClassLoader generate(Set<String> jarPaths) {
        List<URL> urls = new ArrayList<>();
        for (String path : jarPaths) {
            try {
                urls.add(new File(path).toURI().toURL());
            } catch (MalformedURLException e) {

            }
        }
        Logger.info("Generate  URLClassLoader using jarPaths: " + AppGson.getInstance().getGson().toJson(jarPaths));
        return new URLClassLoader(urls.toArray(new URL[0]), SyncModelJarClassLoaderFactory.class.getClassLoader());
    }
}
// 使用自定义的 ClassLoader 进行反序列化的 ObjectInputStream
public class CustomClassLoaderObjectInputStream extends ObjectInputStream {
    private final ClassLoader customClassLoader;

    public CustomClassLoaderObjectInputStream(InputStream in, ClassLoader classloader) throws IOException {
        super(in);
        this.customClassLoader = classloader;
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        try {
            Class<?> result = Class.forName(desc.getName(), true, customClassLoader);
            Logger.info("resolve class " + desc.getName() + " from customClassLoader succeeded");
            return result;
        } catch (Throwable e) {
            // ignore
        }
        return super.resolveClass(desc);
    }
}

缓存清理

如前文所述,为了避免缓存过期导致的错误,需要有合理的缓存淘汰机制。

Sync 优化方案中,会在配置文件中记录生成缓存时的:

  • Gradle 版本

  • Android Gradle Plugin 版本

  • Java 版本

  • Kotlin 版本

  • Android Studio 版本

当上述任一版本发生变更时,便会将内存、磁盘缓存进行清理并重新生成。

之所以选择上述工具的版本:

  • 一方面是因为这些工具代表了 Sync 时的基础构建信息,会在若干 Tooling Model 中进行记录(如 BuildEnvironment、KotlinGradleModel 等)

  • 另一方面,上述工具中均包含了若干 Tooling Model 的构建逻辑,当版本变更时 Tooling Model 的构建逻辑也存在潜在的变更可能,为了保证正确性必须将先前版本的缓存进行清理

当然,上述的缓存淘汰机制仅覆盖了 Android Studio Sync 时的绝大多数场景,由于 Intellij IDEA Platform API 的开放性,任意的 IDE 插件均可注入自己的 Tooling Model 逻辑,所以可能存在遗漏的可能性。

好在本方案在飞书和今日头条项目已上线数周,暂无 Sync 相关的错误反馈。后续,我们将针对 Gradle 以及 Android Studio 版本更新进行持续兼容迭代。

文件下载优化

无论从 Android Studio 底部进度条的提示还是前文的 Chrome Trace,我们都可以直观的看到,当我们首次 Sync 或工程依赖发生变更时,依赖对应的文件下载都会占据 Sync 过程中的相当大的比例。在这一节中,我们着重讲解优化方案中对于这一操作的优化原理。

进行文件下载的 Tooling Model

如前文所述,Sync 过程的主要操作都是通过构建各类 Tooling Model 来实现的,而文件下载也不例外。Sync 过程中触发文件下载的主要是如下两个 Tooling Model:

  • BuildScriptClasspathModel

  • AdditionalClassifierArtifactsModel

其中 BuildScriptClasspathModel 对应构建脚本中各个 Gradle 插件相关的文件,AdditionalClassifierArtifactsModel 对应模块的编译时和运行时依赖相关的文件。

知晓了文件下载的代码实现所在,我们便可以有的放矢,深入其中进行优化。

禁用插件的 sources.jar 下载

对于绝大多数开发者来说,我们极少查看 Gradle 插件的源码,但前文中的 BuildScriptClasspathModel 却会在 Sync 时下载这部分依赖的 sources.jar 文件,占据了不少耗时:

// ModelBuildScriptClasspathBuilderImpl.java

public Object buildAll(@NotNull final String modelName, @NotNull final Project project, @NotNull ModelBuilderContext context) {
  ...
  boolean downloadJavadoc = false;
  boolean downloadSources = true;
  ...

  Collection<ExternalDependency> dependencies = new DependencyResolverImpl(project, downloadJavadoc, downloadSources, mySourceSetFinder).resolveDependencies(classpathConfiguration);
  ...
  return buildScriptClasspath;
}

可以看到,该 ModelBuilder 可以下载 Gradle 插件的 javadoc 和 sources.jar 文件,前者默认不下载,后者默认下载。

既然绝大多数场景下我们无需查看这部分源码,那么我们能否直接禁用该行为呢?

答案是肯定的。代码中该 ModelBuilder 读取了 project 中 IdeaPlugin 扩展中的对应配置并为其前文的标识位赋值:

final IdeaPlugin ideaPlugin = project.getPlugins().findPlugin(IdeaPlugin.class);
if (ideaPlugin != null) {
  final IdeaModule ideaModule = ideaPlugin.getModel().getModule();
  downloadJavadoc = ideaModule.isDownloadJavadoc();
  downloadSources = ideaModule.isDownloadSources();
}

于是在 Sync 优化方案中,我们便直接通过该扩展进行配置,禁用了此处的 sources.jar 的下载:

project.getRootProject().allprojects(p -> {
    IdeaPlugin ideaPlugin = p.getPlugins().findPlugin(IdeaPlugin.class);
    if (ideaPlugin != null) {
        IdeaModule ideaModule = ideaPlugin.getModel().getModule();
        ideaModule.setDownloadSources(false);
        SyncLogger.i("Disable BuildScriptClasspathModel downloading sources.jar for project " + p.getDisplayName());
    }
});

禁用不存在 sources.jar 的依赖的 sources.jar 查询

并非所有依赖都在 maven 仓库中上传了 sources.jar 文件。Gradle 在下载之前会通过一次 HEAD 请求判断目标文件是否存在。

通过对于 Sync 过程的网络请求打点,我们发现返回码为 404 的 HEAD 请求耗时尤为明显。由于公司内 maven 代理仓库的存在,当资源不存在时,会遍历所有的仓库确认所有被代理的仓库中均不存在该资源才会返回 404。

显然,我们可以通过实现收集不存在 sources.jar 的依赖列表,然后在实际 Sync 时跳过这部分资源的查询来节省这部分耗时。

Gradle 中在如下代码处进行外部资源的查询,我们通过 UnresolvedArtifactCollector 记录所有查询失败的组件坐标以及资源类型并将其中 sources.jar 类型不存在的组件写入配置文件中即可:

// ExternalResourceResolver.java
protected Set<ModuleComponentArtifactMetadata> findOptionalArtifacts(ModuleComponentResolveMetadata module, String type, String classifier) {
    ModuleComponentArtifactMetadata artifact = module.artifact(type, "jar", classifier);
    if (createArtifactResolver(module.getSources()).artifactExists(artifact, new DefaultResourceAwareResolveResult())) {
        return ImmutableSet.of(artifact);
    }
    UnresolvedArtifactCollector.getInstance().record(module.getId().getDisplayName(), type);
    return Collections.emptySet();
}

如前文所述,实际执行 sources.jar 下载的是 Tooling Model AdditionalClassifierArtifactsModel,在其对应的 ToolingModelBuilder 中通过如下方式触发了文件下载并将最终的本地文件地址返回给 Android Studio:

// AdditionalClassifierArtifactsModelBuilder.kt

override fun buildAll(modelName: String, parameter: AdditionalClassifierArtifactsModelParameter, project: Project): Any {
  // Collect the components to download Sources and Javadoc for. DefaultModuleComponentIdentifier is the only supported type.
  // See DefaultArtifactResolutionQuery::validateComponentIdentifier.
  ...
  try {
    // Create query for Maven Pom File.
    val pomQuery = project.dependencies.createArtifactResolutionQuery()
      .forComponents(ids)
      .withArtifacts(MavenModule::class.java, MavenPomArtifact::class.java)

    // Map from component id to Pom File.
    val idToPomFile = pomQuery.execute().resolvedComponents.map {
      it.id.displayName to getFile(it, MavenPomArtifact::class.java)
    }.toMap()

    // Create map from component id to location of sample sources file.
    val idToSampleLocation: Map<String, File?> =
      if (parameter.downloadAndroidxUISamplesSources) {
        getSampleSources(parameter, project)
      }
      else {
        emptyMap()
      }

    // Create query for Javadoc and Sources.`
    val docQuery = project.dependencies.createArtifactResolutionQuery()
      .forComponents(ids)
      .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java, JavadocArtifact::class.java)

    artifacts = docQuery.execute().resolvedComponents.filter { it.id is ModuleComponentIdentifier }.map {
      val id = it.id as ModuleComponentIdentifier
      AdditionalClassifierArtifactsImpl(
        ArtifactIdentifierImpl(id.group, id.module, id.version),
        getFile(it, SourcesArtifact::class.java),
        getFile(it, JavadocArtifact::class.java),
        idToPomFile[it.id.displayName],
        idToSampleLocation[it.id.displayName]
      )
    }
  }
  catch (t: Throwable) {
    message = "Unable to download sources/javadoc: " + t.message
  }
  return AdditionalClassifierArtifactsModelImpl(artifacts, message)
}

由于这部分代码位于 Android Studio 代码内部,我们难以更改其实现。但我们可以通过其调用的 Gradle API createArtifactResolutionQuery 在 Gradle 层面对于依赖的文件下载行为进行干预:

// DefaultArtifactResolutionQuery.java
public interface Interceptor {
    boolean intercept(ComponentIdentifier componentId, Class<? extends Artifact> artifact);
}

private static Interceptor sInterceptor;

public static void setInterceptor(Interceptor interceptor) {
    sInterceptor = interceptor;
}

private ComponentArtifactsResult buildComponentResult(ComponentIdentifier componentId, ComponentMetaDataResolver componentMetaDataResolver, ArtifactResolver artifactResolver) {
    ...
    for (Class<? extends Artifact> artifactType : artifactTypes) {
        if (sInterceptor != null && sInterceptor.intercept(componentId, artifactType)) {
            continue;
        }
        addArtifacts(componentResult, artifactType, component, artifactResolver);
    }
    return componentResult;
}

在这里,我们通过外部注入一个拦截器(Interceptor)的方式对于 Sync 过程中的文件下载行为进行干预。

在框架层面,便是通过前文生成的配置文件对于文件查询进行拦截:

// SyncArtifactResolutionQueryInterceptor.java
public boolean intercept(@NotNull ComponentIdentifier componentIdentifier, @NotNull Class<? extends Artifact> artifactType) {
    if (artifactType == JavadocArtifact.class) {
        return true;
    }
    String gav = componentIdentifier.getDisplayName();
    if (CollectionUtils.isNotEmpty(noSourcesJarSet) && artifactType == SourcesArtifact.class) {
        String ga = NoSourcesJarConfiguration.gav2ga(gav);
        boolean intercept = noSourcesJarSet.contains(ga);
        if (intercept) {
            SyncLogger.i("Intercept sources download of " + gav);
            return true;
        }
    }
    return false;
}

这里为了进一步提升性能,还对 javadoc 文件的下载进行了拦截。

并发文件下载

通过观察前文的 Chrome Trace,我们发现 AdditionalClassifierArtifactsModel 对于文件的下载均为串行执行的。显然,这些资源文件下载是不存在逻辑上的前后依赖关系的,如果能够实现文件的并发下载显然能够显著提升该 Tooling Model 的构建效率。

ec10d4b5230e97667a5770df7775d673.png

AdditionalClassifierArtifactsModel 中的串行文件下载

实际上,Sync 过程中对于 aar 文件的下载会通过 ParallelResolveArtifactSet 进行包装,最终通过 BuildOperationExecutor#runAll 方法实现并发下载:

// ParallelResolveArtifactSet.java
public void visit(final ArtifactVisitor visitor) {
    // Start preparing the result
    StartVisitAction visitAction = new StartVisitAction(visitor);
    buildOperationProcessor.runAll(visitAction);

    // Now visit the result in order
    visitAction.result.visit(visitor);
}

那么,我们便也可以依样画葫芦,将 AdditionalClassifierArtifactsModel 中的文件下载行为改造为并发:

// ByteGradle 中的 DefaultArtifactResolutionQuery.java
private ArtifactResolutionResult createResult(ComponentMetaDataResolver componentMetaDataResolver, ArtifactResolver artifactResolver) {
    Set<ComponentResult> componentResults = Sets.newHashSet();
    if (!sIsParallelQuery) {
        ...
    } else {
        Set<ComponentResult> resultSet = new ParallelResolutionQueryArtifactSet(
            componentMetaDataResolver,
            artifactResolver,
            componentIds,
            artifactTypes,
            buildOperationExecutor,
            this::createResult
        ).execute();
        componentResults.addAll(resultSet);
    }

    return new DefaultArtifactResolutionResult(componentResults);
}

改造后 AdditionalClassifierArtifactModel 的构建效率显著提升:

b2d4a3da75807c498e3d127805be277c.png

优化后的并发文件下载

过滤 test 变体

除了上述进行文件下载的 Tooling Model 外,由 Android Gradle Plugin 提供的 Tooling Model Variant 的耗时也较为明显。

查看 Variant 构建的源码,我们会发现其有不少用于处理 test 相关的逻辑:

private VariantImpl createVariant(@NonNull ComponentPropertiesImpl componentProperties) {
    ...
    if (componentProperties instanceof VariantPropertiesImpl) {
        VariantPropertiesImpl variantProperties = (VariantPropertiesImpl) componentProperties;

        for (VariantType variantType : VariantType.Companion.getTestComponents()) {
            ComponentPropertiesImpl testVariant =
                    variantProperties.getTestComponents().get(variantType);
            if (testVariant != null) {
                switch ((VariantTypeImpl) variantType) {
                    case ANDROID_TEST:
                        extraAndroidArtifacts.add(
                                createAndroidArtifact(
                                        variantType.getArtifactName(), testVariant));
                        break;
                    case UNIT_TEST:
                        clonedExtraJavaArtifacts.add(
                                createUnitTestsJavaArtifact(variantType, testVariant));
                        break;
                    default:
                        throw new IllegalArgumentException(
                                "Unsupported test variant type ${variantType}.");
                }
            }
        }
    }

    // used for test only modules
    Collection<TestedTargetVariant> testTargetVariants =
            getTestTargetVariants(componentProperties);

    checkProguardFiles(componentProperties);

    return new VariantImpl(
            variantName,
            componentProperties.getBaseName(),
            componentProperties.getBuildType(),
            getProductFlavorNames(componentProperties),
            new ProductFlavorImpl(
                    variantDslInfo.getMergedFlavor(), variantDslInfo.getApplicationId()),
            mainArtifact,
            extraAndroidArtifacts,
            clonedExtraJavaArtifacts,
            testTargetVariants,
            inspectManifestForInstantTag(componentProperties),
            getDesugaredMethods(componentProperties));
}

大多数场景下,我们在 Sync 时并不关心 test 变体相关的信息,显然我们可以通过过滤掉 test 变体来节省一部分时间。

基于 Gradle 的类加载机制(在 classpath 中先声明的依赖中的类会在运行时最终生效),我们可以在自己的插件中定义与 Android Gradle Plugin 中同名的类并先于 Android Gradle Plugin 进行声明来达到“类覆盖”的效果。

这里为了过滤 test 相关的变体,我们直接将 VariantManager 中的相关逻辑进行了重写:

// VariantManager.java
public ComponentInfo<
        TestComponentImpl<? extends TestComponentPropertiesImpl>,
        TestComponentPropertiesImpl>
createTestComponents(
        @NonNull DimensionCombination dimensionCombination,
        @NonNull BuildTypeData<BuildType> buildTypeData,
        @NonNull List<ProductFlavorData<ProductFlavor>> productFlavorDataList,
        @NonNull VariantT testedVariant,
        @NonNull VariantPropertiesT testedVariantProperties,
        @NonNull VariantType variantType) {
    if (VariantUtils.shouldDisableTest(project)) {
        SyncLogger.i("Disable TestVariantData for project " + project.getDisplayName());
        return null;
    }
    ...
}

这样,我们便从根本上过滤了 test 相关的变体。

多管齐下,效果如何

通过对 Gradle Sync 过程进行细致深入的分析,我们构思并开发了前文所述的多个 Sync 优化手段,并将数据收集与上报能力整合,形成了一整套数据驱动、多管齐下的解决方案。

目前,该方案已经在公司内的头部应用飞书和今日头条等项目上线,均取得了逾 50% 的收益,Sync 耗时 50 分位值控制在 1min 以内,90 分位值约 3min,优化效果明显。

该方案取得这样的成绩,离不开飞书和今日头条团队相关同学的支持,也离不开抖音、TikTok 等多个团队同学提供的灵感,该方案的成功无疑是站在了巨人的肩膀上方能取得,向上述团队的同学致敬!

未完待续

虽然已经取得了不错的收益,但我们对于开发者体验提升的探索不会止步于此:从 Gradle 层面来说,除了前文提到的 Tooling Model ,尚有不少 Tooling Model 的构建逻辑值得分析;从 Android Studio 层面来说,除了 Gradle Sync,Indexing、编码效率、代码索引等诸多功能的体验仍有提升空间······

研发工具优化的世界尚有未知值得探索,开发者体验提升的故事仍旧未完待续。

如果你也对此感兴趣,欢迎加入我们,携手共建一流的开发者体验。

加入我们

我们是字节跳动终端技术团队(Client Infrastructure)下的 DevOps - Build 团队,负责打造公司范围内,面向不同业务场景的研发工具,提升移动应用研发效率。目前急寻 Android 移动研发工程师 / iOS 移动研发工程师 / 服务端研发工程师。

了解更多信息请联系:sunjuntao.rex@bytedance.com,邮件主题 简历-姓名-求职意向-期望城市-电话。

相关推荐

2024 抖音欢笑中国年(五):Wasm、WebGL 在互动技术中的创新应用

前言随着 Web 前端技术的不断发展,越来越多的新兴技术方案被引入到 Web 开发中,其中 Wasm 和 WebGL 作为前端领域的两大利器,为开发者带来了更多的可能性。本文将结合2024 年抖音欢笑中国年的部分项目,重点介绍如何利用 Wasm 和 WebGL 对目前流行的一些前端互动技术(比如 Lottie、渲染引擎、动画图片等)进行创新和实践,利用 Wasm 和 WebGL 等新技术方案的特性...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(四):渲染技术实践与探索

作者:陈瑞、欧阳浩铸、王武俊、倪梵云前言抖音在2024年春节期间推出了欢笑中国年系列活动,为用户带来了全新的体验和乐趣。而SAR Creator则为该项目研发工作提供了重要的技术支持。SAR Creator是一款基于 Typescript 的高性能、轻量化的互动解决方案,目前支持了浏览器和跨端框架平台,服务于字节内部的各种互动业务。这些绚烂多彩的互动场景当然也离不开实时渲染技术的支持,因此本文将专...

字节跳动技术团队官方博客 2万+

2024 抖音欢笑中国年(三):编辑器技巧与实践

前言本次春节活动中,我们大部分场景使用内部的 SAR Creator互动方案来实现。SAR Creator 是一款基于 TypeScript 的高性能、轻量化的互动解决方案,目前支持了Web和字节内部跨端框架平台,服务于字节内部的各种互动业务,包括但不限于抖音春节、抖音直播礼物、抖音UG活动等。SAR Creator 编辑器支持了图形化界面,提供了各类完善的系统(光照、动画、脚本等)供用户快速便捷...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(二):AnnieX互动容器创新玩法解析

本文基于24年抖音春节活动业务背景,介绍了字节跨端容器AnnieX在游戏互动套件上的探索,致力于提升容器在游戏互动场景的优化能力。业务背景AnnieX作为字节一方游戏统一容器,服务字节内部电商、直播、UG等跨端场景业务。在字节一方游戏互动场景,有大量的一方游戏业务对容器有特定的流量、端能力和游戏优化的诉求。因此我们不断深入互动游戏业务特点,为字节游戏提供完善游戏端能力和流量运营能力,同时提供游戏互...

字节跳动技术团队官方博客 1万+

2024 抖音欢笑中国年(一):招财神龙互动技术揭秘

字节跳动旗下的抖音等 App 在 2024 年春节期间推出了欢笑中国年系列活动,在实现增长业务目标的同时,为用户带来了全新的体验和乐趣。「招财神龙」是其中的一个重要玩法。前言本次春节活动,使用到了字节内的主要前端、跨端、互动技术产品。主要涉及:跨端框架提供了首屏直出的方案使其具有较短的首屏时间,能够大大提升业务加载成功率。跨端框架也提供了 Canvas 作为 SAR Creator 等渲染引擎的...

字节跳动技术团队官方博客 1万+

Monorepo 解决方案 — 基于 Bazel 的 Xcode 性能优化实践

背景介绍书接上回《Monorepo 解决方案 — Bazel 在头条 iOS 的实践》,在头条工程切换至 Bazel 构建系统后,为了支持用户使用 Xcode 开发的习惯,我们使用了开源项目 Tulsi 作为生成工具,用于将 Bazel 工程转换为 Xcode 工程。但是在使用的过程中,我们发现了一些问题,其中影响较大的是,Xcode 工程卡顿:对于头条这种大型项目来说,Xcode 卡顿一直是本地...

字节跳动技术团队官方博客 1万+

抖音 ANR 自动归因平台建设实践

背景介绍本文在 2024 年初最新一期『抖音客户端基础技术大揭秘』技术沙龙活动中已做过专题分享,本次将内容重新整理文章进行分享。公众号后台回复技术沙龙可查看沙龙回放及 PPT~抖音作为一个超大型的应用,我们在 ANR 问题治理上面临着很大的挑战。首先对于存量问题的优化,由于缺少有效的归因手段,一些长期的疑难问题一直难以突破解决,例如长期位于 Top 1 的 nativePollOnce 问题。同时...

字节跳动技术团队官方博客 2万+

CVPR 2024 | CAMixerSR 动态注意力分配的超分辨率加速框架

背景随着相关技术和应用的发展,比如超高清屏幕、虚拟现实(VR)等沉浸式体验的增加,用户对超高分辨率图像和视频的需求变得越来越强烈。在这些场景中,图像的质量和清晰度对于提供最佳的用户体验至关重要。超高分辨率不仅能提供更清晰、更真实的视觉效果,还能在一定程度上增强人们的互动和沉浸感,在一些VR场景中我们需要8K甚至16K的才可以满足需求。然而要生成或者处理这些超高分辨率的内容,对算力的要求也是与日增长...

字节跳动技术团队官方博客 1万+

CVPR 2024 | Modular Blind Video Quality Assessment:模块化无参视频质量评估

无参视频质量评估 (Blind Video Quality Assessment,BVQA) 在评估和改善各种视频平台并服务用户的观看体验方面发挥着关键作用。当前基于深度学习的模型主要以下采样/局部块采样的形式分析视频内容,而忽视了实际空域分辨率和时域帧率对视频质量的影响,随着高分辨率和高帧率视频投稿逐渐普及,特别是跨分辨率/帧率视频转码档位画质评估场景中,这种影响变得更加不可忽视。在本文中,我们...

字节跳动技术团队官方博客 1万+

2024 AI & 前端:回首展望,光芒未至,破晓之前!

前言回望 2023 年,ChatGPT 的突然爆火,让 AI 无疑成为最为值得注目的新兴领域之一,我们也一起见证了生成式 AI 的寒武纪大爆发。这一年来,国内外的生成式 AI 、大模型和相关产品以令人眼花缭乱的速度更新迭代,新的创业浪潮风起云涌。在这 AI 浪潮下,也让我们有了新的开发思考,探索着在各个环节中“前端 & AI”的应用场景。勇于探索的前端开发者们已经开始挥舞着 AI 的“魔法...

字节跳动技术团队官方博客 1万+

Kotlin 云端差分缓存技术

本文由字节跳动 Buildinfra 团队出品。在我们的工程上线 Monorepo 全源码后,Kotlin 编译成了整个编译中最耗时的步骤,全源码过程中大量的 BuildCache Miss 导致我们的编译数据落后原来多仓二进制时代很多,且业界没有相关的解决方案。本篇文章我们来具体阐述下 BuildInfra 团队自研的解决方案 - Kotlin 云端差分方案的原理和技术实现。一、Monorepo...

字节跳动技术团队官方博客 1万+

字节跳动基础架构SRE-Copilot获得2023 CCF国际AIOps挑战赛冠军

近日,2023 CCF国际AIOps挑战赛决赛暨“大模型时代的AIOps”研讨会在北京成功举办,活动吸引了来自互联网、运营商、科研院所、高校、软硬件厂商等领域多名专家学者参与,为智能运维的前沿学术研究、落地生产实践打开了新思路。决赛中,从初赛两百多支队伍中脱颖而出的十支入围队伍分别展示了各自的方案,并进行了现场答辩,评审专家从选题方向、创新性、实用性、完整度和实验复现结果等多角度进行了综合评定,最...

字节跳动技术团队官方博客 2万+

字节跳动百万级Metrics Agent性能优化的探索与实践

背景metricserver2 (以下简称Agent)是与字节内场时序数据库 ByteTSD 配套使用的用户指标打点 Agent,用于在物理机粒度收集用户的指标打点数据,在字节内几乎所有的服务节点上均有部署集成,装机量达到百万以上。此外Agent需要负责打点数据的解析、聚合、压缩、协议转换和发送,属于CPU和Mem密集的服务。两者结合,使得Agent在监控全链路服务成本中占比达到70%以上,对Ag...

字节跳动技术团队官方博客 1万+

西瓜视频RenderThread引起的闪退问题攻坚历程

背景影响西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了...

字节跳动技术团队官方博客 2万+

字节电商双11 大促容量保障是如何做的?

前言Rhino 简介Rhino是字节自研全链路容量评估产品,致力于构建完整的全链路容量评估解决方案(覆盖:容量预估->资源准备->数据准备->容量验证->监控->分析->决策->处理反馈);围绕容量在稳定性、成本、效率 三方面提供业务全方位基础支撑。Rhino 目前已经成为字节各业务容量评估主流解决方案,并且历年来在业务大型活动稳定性保障中(抖音春节项目、...

字节跳动技术团队官方博客 10万+

使用火山引擎 APMPlus 解决抖音Top 1 Java 崩溃的通用优化方案

背景近3个月,抖音 Android 版面临一个多次触发线上报警的崩溃问题,全量版本和灰度版本的异常数据激增,该问题不仅容易触发报警,更成为了 Java Top 1 崩溃问题,带来巨大困扰,急需攻坚解决。本文展现了具体的分析过程、优化思路和解决方案,同时提供了已集成该方案的实用工具。初步分析多维特征我们以某发版期间数据为例进行分析:机型方面:比较分散,有聚集部分samsung sm-s9180 占比...

字节跳动技术团队官方博客 2万+

用 Addon 增强 Node.js 和 Electron 应用的原生能力

前言Node.js Addon 是 Node.js 中为 JavaScript 环境提供 C/C++ 交互能力的机制。其形态十分类似 Java 的 JNI,都是通过提供一套 C/C++ SDK,用于在 C/C++ 中创建函数方法、进行数据转换,以便 JavaScript / Java 等语言进行调用。这样编写的代码通常叫做 Bindings。此外还有基于 C ABI Calling Convent...

字节跳动技术团队官方博客 2万+

火山引擎 ByteHouse 的增强型数据导入技术实践

作为企业数字化建设的必备要素,易用的数据引擎能帮助企业提升数据使用效率,更好提升数据应用价值,夯实数字化建设基础。数据导入是衡量OLAP引擎性能及易用性的重要标准之一,高效的数据导入能力能够加速数据实时处理和分析的效率。作为一款OLAP引擎,火山引擎云原生数据仓库ByteHouse源于开源ClickHouse,在字节跳动多年打磨下,提供更丰富的能力和更强性能,能为用户带来极速分析体验,支撑实时数据...

字节跳动技术团队官方博客 1万+

打造企业级智能问答系统的秘密:如何使用云数据库 PostgreSQL 版实现向量检索

本文就如何利用火山引擎云数据库 PostgreSQL 版和大语言模型技术(Large Language Model,简称 LLM),实现企业级智能交互式问答系统进行介绍。通过本文,你将会了解交互式问答系统的原理,学习 PostgreSQL 的向量化存储和检索技术,以及大语言模型交互技术等。背景在大数据的浪潮下,众多企业建立了自己的知识库,以便于信息检索和知识查询。然而,随着知识库内容的膨胀,传统的...

字节跳动技术团队官方博客 2万+

抖音大型直播的画质优化实践

面临挑战随着抖音内容生态的不断丰富,越来越多的大型赛事在抖音平台进行直播,世界杯/春晚/亚运会等各项赛事节目引来大量用户观看。卡塔尔世界杯期间,抖音提供的稳定高质直播画面为观众带来了完美的观赛体验,决赛的 PCU 高达 3700W+。不同赛事节目涉及链路众多,且不同赛事之间存在差异,如何保障各链路的画质稳定并进一步提升画质,是一个巨大的挑战。如何应对挑战?画质优化链路大型赛事直播涉及链路较长,不同...

字节跳动技术团队官方博客 2万+

抖音直播新一代BVC编码器正式亮相

面临挑战在直播行业发展如火如荼的今天,用户对视频体验的要求也水涨船高。视频基础体验的关键要素包括清晰度、流畅度、低延迟等,而这些要素的“第一性原理”,就是视频本身的编码效率,也就是压缩率。视频编码是整个技术体系的基座,编码效率的显著提升,能够在同等码率下极大提高画质,从而改善用户体验。视频编码效率的重要性不言而喻,但进一步地提升也并非易事,尤其在直播场景中,对编码速度、延迟、码率控制等方面都有很高...

字节跳动技术团队官方博客 7356

Go Metrics SDK Tag 校验性能优化实践

背景Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK,在字节内数十万服务中集成,应用广泛,因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例,讲述对打点 API 的 hot-path 优化的实践。用户在使用 SDK API 进行打点时,需要传入指标对应的 Tag:tags:=[]m.T{{Name:"foo...

字节跳动技术团队官方博客 1万+

云上智能驾驶三维重建最佳实践

智能驾驶技术的不断发展,正在改变着我们的出行方式和交通系统。作为其中的一个关键技术,三维重建在智能驾驶系统中起着重要的作用。除去车端本身的感知、重建算法,自动驾驶技术的落地与发展需要庞大的云端重建能力支撑,火山引擎多媒体实验室通过行业领先的自研三维重建技术,结合强大的云平台资源与能力,助力相关技术在云端大规模重建、自动标注、真实感仿真等场景的落地与应用。本文重点介绍火山引擎多媒体实验室三维重建技术...

字节跳动技术团队官方博客 6305

火山引擎实时、低延时拥塞控制算法的优化实践

摘要火山引擎智能拥塞控制算法 VICC(Volcano Intelligent Congestion Control)是一种自适应的拥塞控制算法,旨在解决全球不同网络环境下,不同音视频应用对带宽利用率和延时的差异化要求。它结合了传统拥塞控制算法(如 GCC 和 BBR)的优点,并且能够根据不同的网络条件、业务偏好和码率特征进行自适应调整,包括自适应拥塞响应速度、自适应带宽探测幅度、自适应丢包检测策...

字节跳动技术团队官方博客 6735

veImageX 演进之路:Web 图片加载提速50%

背景说明火山引擎veImageX演进之路主要介绍了veImageX在字节内部从2012年随着字节成长过程中逐步演进的过程,演进中包括V1、V2、V3版本并最终面向行业输出;整个演进过程中包括服务端、客户端、网络库、业务场景与优化等多个角度介绍在图像处理压缩、省成本与体验优化的经验与方案;本篇文章重点介绍在web端演进和提供的能力,图片是 Web 站点中的重要元素,图片体积、格式、分辨率以及渲染方式...

字节跳动技术团队官方博客 6589

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路

作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方...

字节跳动技术团队官方博客 7818

如何利用播放器节省20%点播成本

点播成本节省的点其实涉及诸多部分,例如:CDN、转码、存储等,而利用播放器降本却是很多客户比较陌生的部分。火山引擎基于内部支撑抖音集团相关业务的实践,播放器恰恰是成本优化中最重要和最为依赖的部分。火山引擎的视频团队做了份数据统计,在一个很经典的视频业务中,我们在2022年至2023年大约1年半的时间里,针对这个业务进行了33次成本优化点,其中13次是播放器主导的优化,其余的有12次也是需要播放器强...

字节跳动技术团队官方博客 6176

火山引擎 ByteHouse:ClickHouse 如何保证海量数据一致性

背景ClickHouse是一个开源的OLAP引擎,不仅被全球开发者广泛使用,在字节各个应用场景中也可以看到它的身影。基于高性能、分布式特点,ClickHouse可以满足大规模数据的分析和查询需求,因此字节研发团队以开源ClickHouse为基础,推出火山引擎云原生数据仓库ByteHouse。在日常工作中,研发人员经常会遇到业务链路过长,导致流程稳定性和数据一致性难保障的问题,这在分布式、跨服务的场...

字节跳动技术团队官方博客 6900

抖音集团都在用的画质评估工具,确定不试试吗?

导读本文从抖音集团内部画质评估体系的建设历程着笔,主要分享了画质评测对于业务的重要性、主要应用场景和内部产品的一些典型实践案例。通过分享业务视角遇到的一些问题和我们的解决思路,希望能抛砖引玉,为遇到类似困扰的伙伴们提供有价值的参考。画质评估体系建设历程为何评测画质如此重要?我们通过线上业务大量实验发现,图片画质优劣对点击率、 停留时长等消费类指标有正相关影响,间接影响用户收益指标。因此,建设一套行...

字节跳动技术团队官方博客 7940

VLDB 2023 | CDSBen: 字节跳动 veDB 数据库存储系统性能测试模型

背景随着业务爆炸式增长与云原生技术的日渐成熟,大量云原生分布式数据库产品如雨后春笋般涌现,其中一部分主打 OLTP 场景的分布式数据库强调的是从计算-存储分离架构获得弹性收益;对于业界各种计算-存储分离架构的数据库而言,怎么用真实的端到端数据库 workload 去 benchmark 其底层存储系统一直存在以下难题:对于数据库专用存储系统,不存在如 fio 一样的“事实标准” benchmark...

字节跳动技术团队官方博客 5896
上一篇: 字节跳动自研高性能微服务框架 Kitex 的演进之旅
下一篇: 火山引擎 A/B 测试私有化实践
 字节跳动技术团队
字节跳动技术团队 企业官方账号 企业官方账号
博客等级 码龄7年 8153粉丝 316原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值