Baseline Profile 安装时优化在西瓜视频的实践

news2024/12/23 1:16:16

动手点关注

ec8b02da7be4da546d76a577712f8dbc.gif

干货不迷路

背景

在Android上,Java/Kotlin代码会编译为DEX字节码,在运行期由虚拟机解释执行。但是,字节码解释执行的速度比较慢。所以,通常虚拟机会在解释模式基础上做一些必要的优化。

在Android 5,Google采用的策略是在应用安装期间对APP的全量DEX进行AOT优化。AOT优化(Ahead of time),就是在APP运行前就把DEX字节码编译成本地机器码。虽然运行效率相比DEX解释执行有了大幅提高,但由于是全量AOT,就会导致用户需要等待较长的时间才能打开应用,对于磁盘空间的占用也急剧增大。

于是,为了避免过早的资源占用,从Android 7开始便不再进行全量AOT,而是JIT+AOT的混合编译模式。JIT(Just in time),就是即时优化,也就是在APP运行过程中,实时地把DEX字节码编译成本地机器码。具体方式是,在APP运行时分析运行过的热代码,然后在设备空闲时触发AOT,在下次运行前预编译热代码,提升后续APP运行效率。

但是热代码代码收集需要比较长周期,在APP升级覆盖安装之后,原有的预编译的热代码失效,需要再走一遍运行时分析、空闲时AOT的流程。在单周迭代的研发模式下问题尤为明显。

因此,从Android 9 开始,Google推出了Cloud Profiles技术。它的原理是,在部分先安装APK的用户手机上,Google Play Store收集到热点代码,然后上传到云端并聚合。这样,对于后面安装的用户,Play Store会下发热点代码配置进行预编译,这些用户就不需要进行运行时分析,大大提前了优化时机。不过,这个收集聚合下发过程需要几天时间,大部分用户还是没法享受到这个优化。

最终,在2022年Google推出了 Baseline Profiles (https://developer.android.com/topic/performance/baselineprofiles/overview?hl=zh-cn)技术。它允许开发者内置自己定义的热点代码配置文件。在APP安装期间,系统提前预编译热点代码,大幅提升APP运行效率。

35523f60645d1a2ac57e9221a8f6570d.png

不过,Google官方的Baseline Profiles存在以下局限性:

  • Baseline Profile 需要使用 AGP 7 及以上的版本,公司内各大APP的版本都还比较低,短期内并不可用

  • 安装时优化依赖Google Play,国内无法使用

为此,我们开发了一套定制化的Baseline Profiles优化方案,可以适用于全版本AGP。同时通过与国内主流厂商合作,推进支持了安装时优化生效。

方案探索与实现

我们先来看一下官方Baseline Profile安装时优化的流程:

88917218bea787091cb9a196572d086a.png

这里面主要包含3个步骤:

  1. 热点方法收集,通过本地运行设备或者人工配置,得到可读格式的基准配置文本文件(baseline-prof.txt)

  2. 编译期处理,将基准配置文本文件转换成二进制文件,打包至apk内(baseline.prof和baseline.profm),另外Google Play服务端还会将云端profile与baseline.prof聚合处理。

  3. 安装时,系统会解析apk内的baseline.prof二进制文件,根据版本号,做一些转换后,提前预编译指定的热点代码为机器码。

热点方法收集

官方文档(https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile)提到使用Jetpack Macrobenchmark库(https://developer.android.com/macrobenchmark) 和 BaselineProfileRule自动收集热点方法。通过在Android Studio中引入Benchmark module,需要编写相应的Rule触发打包、测试等流程。

从下面源码可以看到,最终是通过profman命令可以收集到app运行过程中的热点方法。

private fun profmanGetProfileRules(apkPath: String, pathOptions: List<String>): String {
    // When compiling with CompilationMode.SpeedProfile, ART stores the profile in one of
    // 2 locations. The `ref` profile path, or the `current` path.
    // The `current` path is eventually merged  into the `ref` path after background dexopt.
    val profiles = pathOptions.mapNotNull { currentPath ->
        Log.d(TAG, "Using profile location: $currentPath")
        val profile = Shell.executeScriptCaptureStdout(
            "profman --dump-classes-and-methods --profile-file=$currentPath --apk=$apkPath"
        )
        profile.ifBlank { null }
    }
    ...
    return builder.toString()
}

所以,我们可以绕过Macrobenchmark库,直接使用profman命令,减少自动化接入成本。具体命令如下:

adb shell profman --dump-classes-and-methods \
--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof \
--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk \
> baseline-prof.txt

生成的baseline-prof.txt文件内容如下:

PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;
PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;
HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)Z
HSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)V
Lanet/channel/e/a$b;
Lcom/bytedance/alliance/services/impl/c;
...

这些规则采用两种形式,分别指明方法和类。方法的规则如下所示:

[FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE]

FLAGS表示 HSP 中的一个或多个字符,用于指示相应方法在启动类型方面应标记为 HotStartup 还是 Post Startup

  • 带有 H 标记表示相应方法是一种“热”方法,这意味着相应方法在应用的整个生命周期内会被调用多次。

  • 带有 S 标记表示相应方法在启动时被调用。

  • 带有 P 标记表示相应方法是与启动无关的热方法。

类的规则,则是直接指明类签名即可:

[CLASS_DESCRIPTOR]

不过这里是可读的文本格式,后续还需要进一步转为二进制才可以被系统识别。

另外,release包导出的是混淆后的符号,需要根据mapping文件再做一次反混淆才能使用。

编译期处理

在得到base.apk的基准配置文本文件(baseline-prof.txt)之后还不够,一些库里面

(比如androidx的库里https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview/src/main/baseline-prof.txt)

也会自带baseline-prof.txt文件。所以,我们还需要把这些子library内附带的baseline-prof.txt取出来,与base.apk的配置一起合并成完整的基准配置文本文件。

接下来,我们需要把完整的配置文件转换成baseline.prof二进制文件。具体是由AGP 7.x内的 CompileArtProfileTask.kt 实现的 :

/**
 * Task that transforms a human readable art profile into a binary form version that can be shipped
 * inside an APK or a Bundle.
 */
abstract class CompileArtProfileTask: NonIncrementalTask() {
...
    abstract class CompileArtProfileWorkAction:
            ProfileAwareWorkAction<CompileArtProfileWorkAction.Parameters>() {

        override fun run() {
            val diagnostics = Diagnostics {
                    error -> throw RuntimeException("Error parsing baseline-prof.txt : $error")
            }
            val humanReadableProfile = HumanReadableProfile(
                parameters.mergedArtProfile.get().asFile,
                diagnostics
            ) ?: throw RuntimeException(
                "Merged ${SdkConstants.FN_ART_PROFILE} cannot be parsed successfully."
            )

            val supplier = DexFileNameSupplier()
            val artProfile = ArtProfile(
                    humanReadableProfile,
                    if (parameters.obfuscationMappingFile.isPresent) {
                        ObfuscationMap(parameters.obfuscationMappingFile.get().asFile)
                    } else {
                        ObfuscationMap.Empty
                    },
                    //need to rename dex files with sequential numbers the same way [DexIncrementalRenameManager] does
                    parameters.dexFolders.asFileTree.files.sortedWith(DexFileComparator()).map {
                        DexFile(it.inputStream(), supplier.get())
                    }
            )
            // the P compiler is always used, the server side will transcode if necessary.
            parameters.binaryArtProfileOutputFile.get().asFile.outputStream().use {
                artProfile.save(it, ArtProfileSerializer.V0_1_0_P)
            }

            // create the metadata.
            parameters.binaryArtProfileMetadataOutputFile.get().asFile.outputStream().use {
                artProfile.save(it, ArtProfileSerializer.METADATA_0_0_2)
            }
        }
    }

这里的核心逻辑就是做了以下3件事:

  1. 读取baseline-prof.txt基准配置文本文件,下文用HumanReadableProfile表示

  2. 将HumanReadableProfile、proguard mapping文件、dex文件作为输入传给ArtProfile

  3. 由ArtProfile生成特定版本格式的baseline.prof二进制文件

ArtProfile类是在profgen子工程内实现的,其中有两个关键的方法:

  • 构造方法:读取HumanReadableProfile、proguard mapping文件、dex文件作为参数,构造ArtProfile实例

  • save()方法:输出指定版本格式的baseline.prof二进制文件

参考链接:

https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:profgen/profgen/src/main/kotlin/com/android/tools/profgen/

至此,我们可以基于profgen开发一个gradle plugin,在编译构建流程中插入一个自定义task,将baseline-prof.txt转换成baseline.prof,并内置到apk的asset目录。

7e2805c037079bd753185f680686c520.png

核心代码如下:

val packageAndroidTask =
    variant.variantScope.taskContainer.packageAndroidTask?.get()
packageAndroidTask?.doFirst {
    var dexFiles = collectDexFiles(variant.packageApplication.dexFolders)
    dexFiles = dexFiles.sortedWith(DexFileComparator()) 
    //基准配置文件的内存表示
    var hrp = HumanReadableProfile("baseline-prof.txt")
    var obfFile: File? = getObfFile(variant, proguardTask)
    val apk = Apk(dexFiles, "")
    val obf =
        if (obfFile != null) ObfuscationMap(obfFile) else ObfuscationMap.Empty
    val profile = ArtProfile(hrp!!, obf, apk)

    val dexoptDir = File(variant.mergedAssets.first(), profDir)

    if (!dexoptDir.exists()) {
        dexoptDir.mkdirs()
    }
    val outFile = File(dexoptDir, "baseline.prof")
    val metaFile = File(dexoptDir, "baseline.profm")
    profile.save(outFile.outputStream(), ArtProfileSerializer.V0_1_0_P)
    profile.save(metaFile.outputStream(), ArtProfileSerializer.METADATA_0_0_2)
  }

自定义task主要包含以下几个步骤:

  1. 解压apk获取dex列表,按照一定规则排序(跟Android的打包规则有关,dex文件名和crc等信息需要和prof二进制文件内的对应上)

  2. 通过ObfuscationMap将baseline-prof.txt文件中的符号转换成混淆后的符号

  3. 通过ArtProfile按照一定格式转换成baseline.prof与baseline.profm二进制文件

其中有两个文件:

  • baseline.prof:包含热点方法id、类id信息的二进制编码文件

  • baseline.profm:用于高版本转码的二进制扩展文件

关于baseline.prof的格式,我们从ArtProfileSerializer.kt的注释可以看到不同Android版本有不同的格式。Android 12 开始需要另外转码才能兼容,详见可以看这个issue:

参考链接:https://issuetracker.google.com/issues/234353689

安装期处理

在生成带有baseline.prof二进制文件的APK之后,再来看一下系统在安装apk时如何处理这个baseline.prof文件(基于Android 13源码分析)。本地测试通过adb install-multiple release.apk release.dm命令执行安装,然后通过Android系统包管理子系统进行安装时优化。

Android系统包管理框架分为3层:

  1. 应用层:应用通过getPackageManager获取PMS的实例,用于应用的安装,卸载,更新等操作

  2. PMS服务层:拥有系统权限,解析并记录应用的基本信息(应用名称,数据存放路径、关系管理等),最终通过binder系统层的installd系统服务进行通讯

  3. Installd系统服务层:拥有root权限,完成最终的apk安装、dex优化

7edf11fa7fa744337f12e148fc12a4e4.png

其中处理baseline.prof二进制文件并最终指导编译生成odex的主要路径如下:

InstallPackageHelper.java#installPackagesLI
    InstallPackageHelper.java#executePostCommitSteps
        ArtManagerService.java#prepareAppProfiles
            Installer.java#prepareAppProfile
                InstalldNativeService.cpp#prepareAppProfile
                    dexopt.cpp#prepare_app_profile
                        ProfileAssistant.cpp#ProcessProfilesInternal
        PackageDexOptimizer.java#performDexOpt
            PackageDexOptimizer.java#performDexOptLI
               PackageDexOptimizer.java#dexOptPath
                   InstalldNativeService.cpp#dexopt
                       dexopt.cpp#dexopt
                           dex2oat.cc

在入口installPackagesLI函数中,经过prepare、scan、Reconcile、Commit 四个阶段后最终调用executePostCommitSteps完成apk安装、prof文件写入、dexopt优化:

private void installPackagesLI(List<InstallRequest> requests) {
         //阶段1:prepare
         prepareResult = preparePackageLI(request.mArgs, request.mInstallResult);
         //阶段2:scan
         final ScanResult result = scanPackageTracedLI(
                            prepareResult.mPackageToScan, prepareResult.mParseFlags,
                            prepareResult.mScanFlags, System.currentTimeMillis(),
                            request.mArgs.mUser, request.mArgs.mAbiOverride);
         //阶段3:Reconcile
         reconciledPackages = ReconcilePackageUtils.reconcilePackages(
                            reconcileRequest, mSharedLibraries,
                            mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
         //阶段4:Commit并安装
         commitRequest = new CommitRequest(reconciledPackages,
                            mPm.mUserManager.getUserIds()); 
         executePostCommitSteps(commitRequest);            
    }

executePostCommitSteps中,主要完成prof文件写入与dex优化:

private void executePostCommitSteps(CommitRequest commitRequest) {
        for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
            final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg();
            final String packageName = pkg.getPackageName();
            final String codePath = pkg.getPath();
 
             //步骤1:prof文件写入
            // Prepare the application profiles for the new code paths.
            // This needs to be done before invoking dexopt so that any install-time profile
            // can be used for optimizations.
            mArtManagerService.prepareAppProfiles(pkg,
                    mPm.resolveUserIds(reconciledPkg.mInstallArgs.mUser.getIdentifier()),
                    /* updateReferenceProfileContent= */ true);

             //步骤2:dex优化,在开启baseline profile优化之后compilation-reason=install-dm
            final int compilationReason =
                    mDexManager.getCompilationReasonForInstallScenario(
                            reconciledPkg.mInstallArgs.mInstallScenario);
            DexoptOptions dexoptOptions =
                    new DexoptOptions(packageName, compilationReason, dexoptFlags);

            if (performDexopt) {
                // Compile the layout resources.
                if (SystemProperties.getBoolean(PRECOMPILE_LAYOUTS, false)) {
                    mViewCompiler.compileLayouts(pkg);
                }

                ScanResult result = reconciledPkg.mScanResult;

                mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
                        null /* instructionSets */,
                        mPm.getOrCreateCompilerPackageStats(pkg),
                        mDexManager.getPackageUseInfoOrDefault(packageName),
                        dexoptOptions);
            }

            // Notify BackgroundDexOptService that the package has been changed.
            // If this is an update of a package which used to fail to compile,
            // BackgroundDexOptService will remove it from its denylist.
            BackgroundDexOptService.getService().notifyPackageChanged(packageName);
            notifyPackageChangeObserversOnUpdate(reconciledPkg);
        }
        PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
                incrementalStorages);
    }

prof文件写入

先来看下prof文件写入流程,主要流程如下图所示:

3756be5aaef52f728338f4c9acbb7c8b.png

其入口在ArtManagerService.java``#``prepareAppProfiles

/**
     * Prepare the application profiles.
     *   - create the current primary profile to save time at app startup time.
     *   - copy the profiles from the associated dex metadata file to the reference profile.
     */
    public void prepareAppProfiles(
            AndroidPackage pkg, @UserIdInt int user,
            boolean updateReferenceProfileContent) {
        try {
            ArrayMap<String, String> codePathsProfileNames = getPackageProfileNames(pkg);
            for (int i = codePathsProfileNames.size() - 1; i >= 0; i--) {
                String codePath = codePathsProfileNames.keyAt(i);
                String profileName = codePathsProfileNames.valueAt(i);
                String dexMetadataPath = null;
                // Passing the dex metadata file to the prepare method will update the reference
                // profile content. As such, we look for the dex metadata file only if we need to
                // perform an update.
                if (updateReferenceProfileContent) {
                    File dexMetadata = DexMetadataHelper.findDexMetadataForFile(new File(codePath));
                    dexMetadataPath = dexMetadata == null ? null : dexMetadata.getAbsolutePath();
                }
                synchronized (mInstaller) {
                    boolean result = mInstaller.prepareAppProfile(pkg.getPackageName(), user, appId,
                            profileName, codePath, dexMetadataPath);
                }
            }
        } catch (InstallerException e) {
        }
    }

其中dexMetadata是后缀为.dm的压缩文件,内部包含primary.prof、primary.profm文件,apk的baseline.prof、baseline.profm会在安装阶段转为成dm文件。

mInstaller.prepareAppProfile最终会调用到dexopt.cpp#prepare_app_profile中,通过fork一个子进程执行profman二进制程序,将dm文件、reference_profile文件(位于设备上固定路径,存储汇总的热点方法)、apk文件作为参数输入:

//frameworks/native/cmds/installd/dexopt.cpp
bool prepare_app_profile(const std::string& package_name,
                         userid_t user_id,
                         appid_t app_id,
                         const std::string& profile_name,
                         const std::string& code_path,
                         const std::optional<std::string>& dex_metadata) {

    // We have a dex metdata. Merge the profile into the reference profile.
    unique_fd ref_profile_fd =
            open_reference_profile(multiuser_get_uid(user_id, app_id), package_name, profile_name,
                                   /*read_write*/ true, /*is_secondary_dex*/ false);
    unique_fd dex_metadata_fd(TEMP_FAILURE_RETRY(
            open(dex_metadata->c_str(), O_RDONLY | O_NOFOLLOW)));
    unique_fd apk_fd(TEMP_FAILURE_RETRY(open(code_path.c_str(), O_RDONLY | O_NOFOLLOW)));
    RunProfman args;
    args.SetupCopyAndUpdate(dex_metadata_fd,
                            ref_profile_fd,
                            apk_fd,
                            code_path);
    pid_t pid = fork();
    if (pid == 0) {
        args.Exec();
    }
    return true;
}
    void SetupCopyAndUpdate(const unique_fd& profile_fd,
                            const unique_fd& reference_profile_fd,
                            const unique_fd& apk_fd,
                            const std::string& dex_location) {
        SetupArgs(...);
    }
    void SetupArgs(const std::vector<T>& profile_fds,
                   const unique_fd& reference_profile_fd,
                   const std::vector<U>& apk_fds,
                   const std::vector<std::string>& dex_locations,
                   bool copy_and_update,
                   bool for_snapshot,
                   bool for_boot_image) {
        const char* profman_bin = select_execution_binary("/profman");
        if (reference_profile_fd != -1) {
            AddArg("--reference-profile-file-fd=" + std::to_string(reference_profile_fd.get()));
        }
        for (const T& fd : profile_fds) {
            AddArg("--profile-file-fd=" + std::to_string(fd.get()));
        }
        for (const U& fd : apk_fds) {
            AddArg("--apk-fd=" + std::to_string(fd.get()));
        }
        for (const std::string& dex_location : dex_locations) {
            AddArg("--dex-location=" + dex_location);
        }
        ...
}

实际上,就是执行了下面的profman命令:

./profman --reference-profile-file-fd=9 \
--profile-file-fd=10 --apk-fd=11 \
--dex-location=/data/app/com.ss.android.article.video-4-JZaMrtO7n_kFe4kbhBBA==/base.apk \
--copy-and-update-profile-key

reference-profile-file-fd指向/data/misc/profile/ref/$package/primary.prof文件,记录当前apk版本的热点方法,最终baseline.prof保存的热点方法信息需要写入到reference-profile文件。

profman二进制程序的代码如下:

class ProfMan final {
 public:
  void ParseArgs(int argc, char **argv) {
    MemMap::Init();

    for (int i = 0; i < argc; ++i) {
      if (StartsWith(option, "--profile-file=")) {
        profile_files_.push_back(std::string(option.substr(strlen("--profile-file="))));
      } else if (StartsWith(option, "--profile-file-fd=")) {
        ParseFdForCollection(raw_option, "--profile-file-fd=", &profile_files_fd_);
      } else if (StartsWith(option, "--dex-location=")) {
        dex_locations_.push_back(std::string(option.substr(strlen("--dex-location="))));
      } else if (StartsWith(option, "--apk-fd=")) {
        ParseFdForCollection(raw_option, "--apk-fd=", &apks_fd_);
      } else if (StartsWith(option, "--apk=")) {
        apk_files_.push_back(std::string(option.substr(strlen("--apk="))));
      } 
      ...
  }
 
 static int profman(int argc, char** argv) {
  ProfMan profman;
  // Parse arguments. Argument mistakes will lead to exit(EXIT_FAILURE) in UsageError.
  profman.ParseArgs(argc, argv);
  // Initialize MemMap for ZipArchive::OpenFromFd.
  MemMap::Init(); 
  ...
   // Process profile information and assess if we need to do a profile guided compilation.
  // This operation involves I/O.
  return profman.ProcessProfiles();
  }

可以看到最后一行调用到profman的ProcessProfiles方法,它里面调用了ProfileAssistant.cpp#ProcessProfilesInternal[https://cs.android.com/android/platform/superproject/+/master:art/profman/profile_assistant.cc;l=30?q=ProcessProfilesInternal],核心代码如下:

ProfmanResult::ProcessingResult ProfileAssistant::ProcessProfilesInternal(
    const std::vector<ScopedFlock>& profile_files,
    const ScopedFlock& reference_profile_file,
    const ProfileCompilationInfo::ProfileLoadFilterFn& filter_fn,
    const Options& options) {
  ProfileCompilationInfo info(options.IsBootImageMerge());
  //步骤1:Load the reference profile.
  if (!info.Load(reference_profile_file->Fd(), true, filter_fn)) {
    return ProfmanResult::kErrorBadProfiles;
  }
   // Store the current state of the reference profile before merging with the current profiles.
   uint32_t number_of_methods = info.GetNumberOfMethods();
   uint32_t number_of_classes = info.GetNumberOfResolvedClasses();

  //步骤2:Merge all current profiles.
  for (size_t i = 0; i < profile_files.size(); i++) {
    ProfileCompilationInfo cur_info(options.IsBootImageMerge());
    if (!cur_info.Load(profile_files[i]->Fd(), /*merge_classes=*/ true, filter_fn)) {
      return ProfmanResult::kErrorBadProfiles;
    }
    if (!info.MergeWith(cur_info)) {
      return ProfmanResult::kErrorBadProfiles;
    }
  }
  // 如果新增方法/类没有达到阈值,则跳过
 if (((info.GetNumberOfMethods() - number_of_methods) < min_change_in_methods_for_compilation) 
     && ((info.GetNumberOfResolvedClasses() - number_of_classes) < min_change_in_classes_for_compilation)) {
      return kSkipCompilation;
   }
  ...
    //步骤3:We were successful in merging all profile information. Update the reference profile.
  ...
  if (!info.Save(reference_profile_file->Fd())) {
    return ProfmanResult::kErrorIO;
  }  
  return options.IsForceMerge() ? ProfmanResult::kSuccess : ProfmanResult::kCompile;
}

这里首先通过ProfileCompilationInfo的load方法,读取reference_profile二进制文件序列化加载到内存。再调用MergeWith方法将cur_profile二进制文件(也就是apk内的baseline.prof)合并到reference_profile文件中,最后调用Save方法保存。

再来看下ProfileCompilationInfo的类结构,可以发现与前面编译期处理提到的ArtProfile序列化格式是一致的。

参考链接:https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:profgen/profgen/src/main/kotlin/com/android/tools/profgen/ArtProfileSerializer.kt

//art/libprofile/profile/profile_compilation_info.h
/**
 * Profile information in a format suitable to be queried by the compiler and
 * performing profile guided compilation.
 * It is a serialize-friendly format based on information collected by the
 * interpreter (ProfileInfo).
 * Currently it stores only the hot compiled methods.
 */
class ProfileCompilationInfo {
 public:
  static const uint8_t kProfileMagic[];
  static const uint8_t kProfileVersion[];
  static const uint8_t kProfileVersionForBootImage[];
  static const char kDexMetadataProfileEntry[];

  static constexpr size_t kProfileVersionSize = 4;
  static constexpr uint8_t kIndividualInlineCacheSize = 5;
  ...
 }

dex优化

分析完prof二进制文件处理流程之后,接着再来看dex优化部分。主要流程如下图所示:

72c487b5b633bedcc5d3580eb609a32b.png

dex优化的入口函数PackageDexOptimizer.java#performDexOptLI,跟踪代码可以发现最终是通过调用dex2oat二进制程序:

//dexopt.cpp
int dexopt(const char* dex_path, uid_t uid, const char* pkgname, const char* instruction_set,
        int dexopt_needed, const char* oat_dir, int dexopt_flags, const char* compiler_filter,
        const char* volume_uuid, const char* class_loader_context, const char* se_info,
        bool downgrade, int target_sdk_version, const char* profile_name,
        const char* dex_metadata_path, const char* compilation_reason, std::string* error_msg,
        /* out */ bool* completed) {
    ...    
    RunDex2Oat runner(dex2oat_bin, execv_helper.get());
    runner.Initialize(...);

    bool cancelled = false;
    pid_t pid = dexopt_status_->check_cancellation_and_fork(&cancelled);
    if (cancelled) {
        *completed = false;
        return 0;
    }
    if (pid == 0) {
        //设置schedpolicy,设置为后台线程
        SetDex2OatScheduling(boot_complete);
        //执行dex2oat命令
        runner.Exec(DexoptReturnCodes::kDex2oatExec);
    } else {
        //父进程等待dex2oat子进程执行完,超时时间9.5分钟.
        int res = wait_child_with_timeout(pid, kLongTimeoutMs);
        if (res == 0) {
            LOG(VERBOSE) << "DexInv: --- END '" << dex_path << "' (success) ---";
        } else {
            //dex2oat执行失败
        }
    }
    // dex2oat ran successfully, so profile is safe to keep.
    reference_profile.DisableCleanup();
    return 0;
}

实际上是执行了如下命令:

/apex/com.android.runtime/bin/dex2oat \
--input-vdex-fd=-1 --output-vdex-fd=11 \
--resolve-startup-const-strings=true \
--max-image-block-size=524288 --compiler-filter=speed-profile --profile-file-fd=14 \
--classpath-dir=/data/app/com.ss.android.article.video-4-JZaMrtO7n_kFe4kbhBBA== \
--class-loader-context=PCL[]{PCL[/system/framework/org.apache.http.legacy.jar]} \
--generate-mini-debug-info --compact-dex-level=none --dm-fd=15 \
--compilation-reason=install-dm

常规安装时不会带上dm-fd和install-dm参数,所以不会触发baseline profile相关优化。

dex2oat用于将dex字节码编译成本地机器码,相关的编译流程如下代码:

static dex2oat::ReturnCode Dex2oat(int argc, char** argv) {
  TimingLogger timings("compiler", false, false);
  // 解析参数
  dex2oat->ParseArgs(argc, argv);
  art::MemMap::Init(); 
  // 加载profile热点方法文件
  if (dex2oat->HasProfileInput()) {
    if (!dex2oat->LoadProfile()) {
      return dex2oat::ReturnCode::kOther;
    }
  }
  //打开输入文件
  dex2oat->OpenFile();
  //准备de2oat环境,包括启动runtime、加载boot class path
  dex2oat::ReturnCode setup_code = dex2oat->Setup();

   //检查profile热点方法是否被加载到内存,并做crc校验
  if (dex2oat->DoProfileGuidedOptimizations()) { 
    //校验profile_compilation_info_中dex的crc与apk中dex的crc是否一致
    dex2oat->VerifyProfileData();
  }
  ...  
  //正式开始编译
  dex2oat::ReturnCode result = DoCompilation(*dex2oat);
  ...  
  return result;
}

这个流程包含:

  • 解析命令行传入的参数

  • 调用LoadProfile()加载profile热点方法文件,保存到profile_compilation_info_成员变量中

  • 准备dex2oat环境,包括启动unstarted runtime、加载boot class path

  • profile相关校验,主要检查profile_compilation_info_中的dex的crc与apk中dex的crc是否一致,方法数是否一致

  • 调用DoCompilation正式开始编译

LoadProfile方法加载profile热点方法文件如下代码:

bool LoadProfile() {
    //初始化profile热点方法的内存对象:profile_compilation_info_
    profile_compilation_info_.reset(new ProfileCompilationInfo());

    //读取reference profile文件列表
    // Dex2oat only uses the reference profile and that is not updated concurrently by the app or
    // other processes. So we don't need to lock (as we have to do in profman or when writing the
    // profile info).
    std::vector<std::unique_ptr<File>> profile_files;
    if (!profile_file_fds_.empty()) {
      for (int fd : profile_file_fds_) {
        profile_files.push_back(std::make_unique<File>(DupCloexec(fd)));
      }
    } 
    ...
    //依次加载到profile_compilation_info_中
    for (const std::unique_ptr<File>& profile_file : profile_files) {
      if (!profile_compilation_info_->Load(profile_file->Fd())) {
        return false;
      }
    }
    return true;
  }

LoadProfile方法,将之前生成的profile文件加载到内存,保存到profile_compilation_info_变量中。

接着调用Compile方法完成odex文件的编译生成,如下代码:

// Set up and create the compiler driver and then invoke it to compile all the dex files.
  jobject Compile() REQUIRES(!Locks::mutator_lock_) {
    ClassLinker* const class_linker = Runtime::Current()->GetClassLinker();
    
    TimingLogger::ScopedTiming t("dex2oat Compile", timings_);
    ...
    compiler_options_->profile_compilation_info_ = profile_compilation_info_.get();

    driver_.reset(new CompilerDriver(compiler_options_.get(),
                                     verification_results_.get(),
                                     compiler_kind_,
                                     thread_count_,
                                     swap_fd_));
    driver_->PrepareDexFilesForOatFile(timings_);
    return CompileDexFiles(dex_files);
  }

profile_compilation_info_作为参数传给了CompilerDriver,在之后的编译过程中将用来判断是否编译某个方法和机器码重排。

CompilerDriver::Compile方法开始编译dex字节码,代码如下:

void CompilerDriver::Compile(jobject class_loader,
                             const std::vector<const DexFile*>& dex_files,
                             TimingLogger* timings) {
  for (const DexFile* dex_file : dex_files) {
    CompileDexFile(this,class_loader,*dex_file,dex_files,
                   "Compile Dex File Quick",CompileMethodQuick);
  }
}

static void CompileMethodQuick(...) {
  auto quick_fn = [profile_index](...) {
    CompiledMethod* compiled_method = nullptr;
    if ((access_flags & kAccNative) != 0) {
        //jni方法编译...
    } else if ((access_flags & kAccAbstract) != 0) {
      // Abstract methods don't have code.
    } else if (annotations::MethodIsNeverCompile(dex_file,
                                                 dex_file.GetClassDef(class_def_idx),
                                                 method_idx)) {
      // Method is annotated with @NeverCompile and should not be compiled.
    } else {
      const CompilerOptions& compiler_options = driver->GetCompilerOptions();
      const VerificationResults* results = driver->GetVerificationResults();
      MethodReference method_ref(&dex_file, method_idx);
      // Don't compile class initializers unless kEverything.
      bool compile = (compiler_options.GetCompilerFilter() == CompilerFilter::kEverything) ||
         ((access_flags & kAccConstructor) == 0) || ((access_flags & kAccStatic) == 0);
      // Check if it's an uncompilable method found by the verifier.
      compile = compile && !results->IsUncompilableMethod(method_ref);
      // Check if we should compile based on the profile.
      compile = compile && ShouldCompileBasedOnProfile(compiler_options, profile_index, method_ref);
      if (compile) {
        compiled_method = driver->GetCompiler()->Compile(...);
      }
    }
    return compiled_method;
  };
  CompileMethodHarness(self,driver,code_item,access_flags,
                       invoke_type,class_def_idx,class_loader,
                       dex_file,dex_cache,quick_fn);
}

在CompileMethodQuick方法中可以看到针对不同的方法(jni方法、虚方法、构造函数等)有不同的处理方式,常规方法会通过ShouldCompileBasedOnProfile来判断某个method是否需要被编译。

具体判断条件如下:

// Checks whether profile guided compilation is enabled and if the method should be compiled
// according to the profile file.
static bool ShouldCompileBasedOnProfile(const CompilerOptions& compiler_options,
                                        ProfileCompilationInfo::ProfileIndexType profile_index,
                                        MethodReference method_ref) {
  if (profile_index == ProfileCompilationInfo::MaxProfileIndex()) {
    // No profile for this dex file. Check if we're actually compiling based on a profile.
    if (!CompilerFilter::DependsOnProfile(compiler_options.GetCompilerFilter())) {
      return true;
    }
    // Profile-based compilation without profile for this dex file. Do not compile the method.
    return false;
  } else {
    const ProfileCompilationInfo* profile_compilation_info =
        compiler_options.GetProfileCompilationInfo();

    // Compile only hot methods, it is the profile saver's job to decide
    // what startup methods to mark as hot.
    bool result = profile_compilation_info->IsHotMethod(profile_index, method_ref.index);
   if (kDebugProfileGuidedCompilation) {
      LOG(INFO) << "[ProfileGuidedCompilation] "
          << (result ? "Compiled" : "Skipped") << " method:" << method_ref.PrettyMethod(true);
    }
    return result;
  }
}

可以看到是依据profile_compilation_info_是否命中hotmethod来判断。我们把编译日志打开,可以看到具体哪些方法被编译,哪些方法被跳过,如下图所示,这与我们配置的profile是一致的。

c5d267d36aa8cd4e2a3d20ac1d358f07.png

机器码生成的实现在CodeGenerator类中,代码如下,具体细节将不再展开。

//art/compiler/optimizing/code_generator.cc
void CodeGenerator::Compile(CodeAllocator* allocator) {
  InitializeCodeGenerationData();
  HGraphVisitor* instruction_visitor = GetInstructionVisitor();
  GetStackMapStream()->BeginMethod(...);

  size_t frame_start = GetAssembler()->CodeSize();
  GenerateFrameEntry();
  if (disasm_info_ != nullptr) {
    disasm_info_->SetFrameEntryInterval(frame_start, GetAssembler()->CodeSize());
  }

  for (size_t e = block_order_->size(); current_block_index_ < e; ++current_block_index_) {
    HBasicBlock* block = (*block_order_)[current_block_index_];
    Bind(block);
    MaybeRecordNativeDebugInfo(/* instruction= */ nullptr, block->GetDexPc());
    for (HInstructionIterator it(block->GetInstructions()); !it.Done(); it.Advance()) {
      HInstruction* current = it.Current();
      DisassemblyScope disassembly_scope(current, *this);
      current->Accept(instruction_visitor);
    }
  }
  GenerateSlowPaths();
  if (graph_->HasTryCatch()) {
    RecordCatchBlockInfo();
  }
  // Finalize instructions in assember;
  Finalize(allocator);
  GetStackMapStream()->EndMethod(GetAssembler()->CodeSize());
}

另外,profile_compilation_info_也会影响机器码重排,我们知道系统在通过IO加载文件的时候,一般都是按页维度来加载的(一般等于4KB),热点代码重排在一起,可以减少IO读取的次数,从而提升性能。

odex文件的机器码布局部分由OatWriter类实现,声明代码如下:

class OatWriter {
 public:
  OatWriter(const CompilerOptions& compiler_options,
            const VerificationResults* verification_results,
            TimingLogger* timings,
            ProfileCompilationInfo* info,
            CompactDexLevel compact_dex_level);
  ...          
  // Profile info used to generate new layout of files.
  ProfileCompilationInfo* profile_compilation_info_;

  // Compact dex level that is generated.
  CompactDexLevel compact_dex_level_;
  using OrderedMethodList = std::vector<OrderedMethodData>;
  ...

从中可以看到profile_compilation_info_会被OatWriter类用到,用于生成odex机器码的布局。

具体代码如下:

// Visit every compiled method in order to determine its order within the OAT file.
// Methods from the same class do not need to be adjacent in the OAT code.
class OatWriter::LayoutCodeMethodVisitor final : public OatDexMethodVisitor {
 public:
  LayoutCodeMethodVisitor(OatWriter* writer, size_t offset)
      : OatDexMethodVisitor(writer, offset),
        profile_index_(ProfileCompilationInfo::MaxProfileIndex()),
        profile_index_dex_file_(nullptr) {
  }

  bool StartClass(const DexFile* dex_file, size_t class_def_index) final {
    // Update the cached `profile_index_` if needed. This happens only once per dex file
    // because we visit all classes in a dex file together, so mark that as `UNLIKELY`.
    if (UNLIKELY(dex_file != profile_index_dex_file_)) {
      if (writer_->profile_compilation_info_ != nullptr) {
        profile_index_ = writer_->profile_compilation_info_->FindDexFile(*dex_file);
      }
      profile_index_dex_file_ = dex_file;
    }
    return OatDexMethodVisitor::StartClass(dex_file, class_def_index);
  }

  bool VisitMethod(size_t class_def_method_index, const ClassAccessor::Method& method){
    OatClass* oat_class = &writer_->oat_classes_[oat_class_index_];
    CompiledMethod* compiled_method = oat_class->GetCompiledMethod(class_def_method_index);

    if (HasCompiledCode(compiled_method)) {
      // Determine the `hotness_bits`, used to determine relative order
      // for OAT code layout when determining binning.
      uint32_t method_index = method.GetIndex();
      MethodReference method_ref(dex_file_, method_index);
      uint32_t hotness_bits = 0u;
      if (profile_index_ != ProfileCompilationInfo::MaxProfileIndex()) {
        ProfileCompilationInfo* pci = writer_->profile_compilation_info_;
        // Note: Bin-to-bin order does not matter. If the kernel does or does not read-ahead
        // any memory, it only goes into the buffer cache and does not grow the PSS until the
        // first time that memory is referenced in the process.
        hotness_bits =
            (pci->IsHotMethod(profile_index_, method_index) ? kHotBit : 0u) |
            (pci->IsStartupMethod(profile_index_, method_index) ? kStartupBit : 0u) 
        }
      }

      OrderedMethodData method_data = {hotness_bits,oat_class,compiled_method,method_ref,...};
      ordered_methods_.push_back(method_data);
    }
    return true;
  }

在LayoutCodeMethodVisitor类中,根据profile_compilation_info_指定的热点方法的FLAG,判断是否打开hotness_bits标志位。热点方法会一起被重排在odex文件靠前的位置。

小结一下,在系统安装app阶段,会读取apk中baselineprofile文件,经过porfman根据当前系统版本做一定转换并序列化到本地的reference_profile路径下,再通过dexoat编译热点方法为本地机器码并通过代码重排提升性能。

厂商合作

Baseline Profile安装时优化需要Google Play支持,但国内手机由于没有Google Play,无法在安装期做实现优化效果。为此,我们协同抖音与小米、华为等主流厂商建立了合作,共同推进Baseline Profile安装时优化在国内环境的落地。具体的合作方式是:

  • 我们通过编译期改造,提供带Baseline Profile的APK给到厂商验证联调。

  • 厂商具体的优化策略会综合考量安装时长、dex2oat消耗资源情况而定,比如先用默认策略安装apk,再后台异步执行Baseline Profile编译。

  • 最后通过Google提供的初步显示所用时间 (TTID) 来验证优化效果(TTID指标用于测量应用生成第一帧所用的时间,包括进程初始化、activity 创建以及显示第一帧。)

参考链接

https://developer.android.com/topic/performance/vitals/launch-time?hl=zh-cn

在与厂商联调的过程中,我们解决了各种问题,其中包括有一个资源压缩方式错误。具体错误信息如下:

java.io.FileNotFoundException: 
This file can not be opened as a file descriptor; it is probably compressed

原来安卓系统要求apk内的baseline.prof二进制是不压缩格式的。我们可以用unzip -v来检验文件是否未被压缩,Defl标志表示压缩,Stored标志表示未压缩。

797a2c8711fc085069fdc612602662da.png

我们可以在打包流程中指定其为STORED格式,即不压缩。

private void writeNoCompress(@NonNull JarEntry entry, @NonNull InputStream from) throws IOException {
    byte[] bytes = new byte[from.available()];
    from.read(bytes);
    entry.setMethod(JarEntry.STORED);
    entry.setSize(bytes.length);
    CRC32 crc32 = new CRC32();
    crc32.update(bytes,0,bytes.length);
    entry.setCrc(crc32.getValue());

    setEntryAttributes(entry);
    jarOutputStream.putNextEntry(entry);
    jarOutputStream.write(bytes, 0, bytes.length);
    jarOutputStream.closeEntry();
}

改完之后我们再检查一下文件是否被压缩。

05633f689951d39431d15fc777880597.png

baseline.prof二进制是不压缩对包体积影响比较小,因为这个文件大部分都是int类型的methodid。经测试,7万+热点方法文件,生成baseline.prof二进制文件62KB,压缩率只有0.1%;如果通过通配符配置,压缩率在5%左右。

一般应用商店下载安装包时在网络传输过程中做了(压缩)https://zh.wikipedia.org/wiki/HTTP%E5%8E%8B%E7%BC%A9处理,这种情况不压缩处理基本不影响包大小,同时不压缩处理也能避免解压缩带来的耗时。

优化效果

在自测中,我们可以通过下面的方式通过install-multiple命令安装APK。

# Unzip the Release APK first
unzip release.apk
# Create a ZIP archive
cp assets/dexopt/baseline.prof primary.prof
cp assets/dexopt/baseline.profm primary.profm
# Create an archive
zip -r release.dm primary.prof primary.profm
# Install APK + Profile together
adb install-multiple release.apk release.dm

在厂商测试中通过下面的命令测试冷启动耗时

PACKAGE_NAME=com.ss.android.article.video
adb shell am start-activity -W -n $PACKAGE_NAME/.SplashActivity | grep "TotalTime"
冷启动Activity耗时比较未优化已优化优化率
荣耀Android11950ms884ms6.9%
小米Android13821ms720ms12.3%

可以看到,在开启Baseline Profile优化之后,首装冷启动(TTID)耗时减少约10%左右,为新用户的启动速度体验带来了极大的提升。

参考文章

  • Android 端内数据状态同步方案VM-Mapping

  • 开源 | Scene:Android 开源页面导航和组合框架

团队介绍

我们是字节跳动西瓜视频客户端团队,专注于西瓜视频 App 的开发和基础技术建设,在客户端架构、性能、稳定性、编译构建、研发工具等方向都有投入。如果你也想一起攻克技术难题,迎接更大的技术挑战,欢迎点击阅读原文,或者投递简历到xiaolin.gan@bytedance.com。

最 Nice 的工作氛围和成长机会,福利与机遇多多,在北上杭三地均有职位,欢迎加入西瓜视频客户端团队 !

ce8ae9b2f565442f4f554099425978d3.png 点击「阅读原文」一键投递!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/604364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python列表:完整介绍与使用指南

Python列表&#xff1a;完整介绍与使用指南 Python是一种非常受欢迎的编程语言&#xff0c;而Python列表是Python编程中最基本的数据结构之一。列表在Python中的使用频率极高&#xff0c;因为列表可以存储许多不同类型的数据&#xff0c;并且可以很方便地进行操作和修改。在本…

Unity Addressables学习笔记(2)---创建远程服务器对象

1.先创建对象 我的做法是&#xff1a; 先拖动一张图片到Resources/img下就是我选中的这张文件夹 2.把图片拖动到Hierarchy里变成一个对象&#xff0c;再把对象拖动到Resources/prefabs里&#xff0c;图片里的单词敲错了哈哈哈哈。 这样这个图片就变成了预制体&#xff0c;然…

AMD在数据中心领域举步维艰,竞争越来越难

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 数据中心业务举步维艰 2023年第一季度&#xff0c;AMD的数据中心业务收入为13亿美元&#xff0c;几乎没有同比增长&#xff0c;反而环比下降了22%。与此同时&#xff0c;它的主要竞争对手英伟达(NVDA)却获得了越来越多的订…

行为型设计模式04-状态模式

✨作者&#xff1a;猫十二懿 ❤️‍&#x1f525;账号&#xff1a;CSDN 、掘金 、个人博客 、Github &#x1f389;公众号&#xff1a;猫十二懿 状态模式 1、状态模式介绍 状态模式&#xff08;State&#xff09;是一种行为型设计模式&#xff0c;当一个对象的内在状态改变时…

python---条件语句(1)

顺序语句 按照写的顺序执行 条件语句 条件语句的一些注意事项: 1.情况1 2.情况2 bbb已经不属于条件语句中的内容了 3.情况3 通常使用4个空格或一个制表符tab来表示! if语句的嵌套 当有多级条件嵌套时,当前的语句属于哪个代码块,完全取决于缩进的级别.

SOFA Weekly|SOFAArk 社区会议预告、Layotto 社区会议回顾与预告、社区本周贡献

SOFA WEEKLY | 每周精选 筛选每周精华问答&#xff0c;同步开源进展 欢迎留言互动&#xff5e; SOFAStack&#xff08;Scalable Open Financial Architecture Stack&#xff09;是蚂蚁集团自主研发的金融级云原生架构&#xff0c;包含了构建金融级云原生架构所需的各个组件&am…

PostgreSql根据给的时间范围统计15分钟粒度、小时粒度、天粒度、周粒度、月粒度工单

PostgreSql根据给的时间范围统计15分钟粒度、小时粒度、天粒度、周粒度、月粒度工单 说明实现15分钟粒度工单统计小时粒度工单统计天粒度工单统计周粒度工单统计月粒度工单统计 说明 项目有个需求是统计故障工单每15分钟、每小时、每天、每周和每月共有多少工单。 这里先做个笔…

JavaEE进阶(统一功能处理)6/2

目录 1.使用拦截器实现用户登录的校验功能 2.统一数据格式的返回 3.统一异常的返回 1.使用拦截器实现用户登录的校验功能 Spring拦截器 关键步骤1.实现HandlerInterceptor接口 2.重写preHeadler方法&#xff0c;在方法中编写自己的业务代码 将拦截器添加到配置文件中&#xf…

【剑指offer】二维数组中的查找(详细解析)

文章目录 题目思路代码实现 题目 题目链接入口&#xff1a;牛客&#xff1a;JZ4 二维数组中的查找 思路 1.核心考点 &#xff08;1&#xff09; 数组相关&#xff1a;二维数组&#xff08;矩阵&#xff09;。 &#xff08;2&#xff09; 特性观察&#xff1a;在一个二维数组…

如何使用宝塔面板搭建网站(最后一步!上传文件至宝塔面板)

这里就是我们搭建宝塔面板的最后一步了&#xff0c;把我们的PHP文件上传到宝塔面板上&#xff0c;就可以通过因特尔网络访问我们的网址了。 第一步&#xff1a;基础安装 必须是Linux服务器 不支持window服务器 最低要求配置1核1G当然再低些也能运行但是不建议 要求服务器环…

值得收藏 | 脑机交互作用研究

神经损伤和疾病对许多人的生活产生了巨大的影响&#xff0c;导致了许多运动障碍和日常任务无法独立完成。皮质假体系统通过脑机接口&#xff08;BCI&#xff09;接收一个动作命令来执行所需的位置&#xff0c;从而使得经历神经损伤的人能够实现部分功能恢复。BCI技术可以在侵入…

chatgpt赋能python:Python创建二维列表的方法

Python创建二维列表的方法 Python是一种高级编程语言&#xff0c;它的灵活性和功能强大的库使其成为数据科学和机器学习的最佳工具之一。其中一个常用的数据结构是二维列表&#xff0c;本文将介绍如何在Python中创建一个二维列表。 什么是二维列表&#xff1f; 在Python中&a…

华为OD机试真题 Java 实现【表示数字】【牛客练习题】

一、题目描述 将一个字符串中所有的整数前后加上符号“*”&#xff0c;其他字符保持不变。连续的数字视为一个整数。 数据范围&#xff1a;字符串长度满足1≤n≤100 。 二、输入描述 输入一个字符串。 三、输出描述 字符中所有出现的数字前后加上符号“*”&#xff0c;其…

arcgis for javascript中的TileLayer(缓存地图服务图层)

一、什么是图层 要理解TileLayer图层&#xff0c;咱要先搞清楚在arcgis中图层的概念&#xff1a; ArcGIS for JavaScript中的图层是数据的可视化链接。简单来说&#xff0c;它可以将数据显示在地图上。 图层是地图上的一个图形单元&#xff0c;可以是点、线、面、文本等&#…

复杂的HANASQL 列表转多行

一 前言 基于HANA的内存数据库的强大性能, SAP建议把业务逻辑下沉到HANA中计算.以便减少应用服务器的负担,让程序性能更好一些. SAP本身的一些复杂的业务逻辑比如MRP运算(MD01)也有了新的事务 MD01N (MRP LIVE),性能得以巨大的提升 报表类的数据分析程序尤其适用此原则. 动态报…

淘宝监控竞品sku数据接口

电商竞品数据监控查询可以通过以下几个步骤实现&#xff1a; 确定需要监控的竞品&#xff1a;首先需要明确自己店铺的产品定位和竞争对手&#xff0c;选择需要监控的竞品。 选择监控工具&#xff1a;根据需求和预算选择适合自己的电商竞品数据监控工具&#xff0c;例如谷歌分析…

nvm管理node的多版本,任意安装,切换不同nodejs版本

1.nvm安装包下载&#xff1a; https://github.com/coreybutler/nvm-windows/releases window操作系统选择安装包直接安装&#xff1a; 如果已经在使用的nodejs无需卸载&#xff0c;安装过程中会提示是否需要管理已经安装的版本&#xff0c;选择“是”。 2.安装完成之后&…

云计算——云计算部署形成及应用

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​ 目录 前言 一.云计算部署形式 1.私有云 &#xff08;1&#xff09;私有云优点 &#x…

计划

img { margin: auto; display: block } 优化器的作用是什么&#xff1f;例举一下神经网络中常用的优化器&#xff1f; 优化器作用&#xff1a;求出让损失函数最小化的参数。 常用优化器&#xff1a; 1、Adam 关联所有样本的梯度&#xff0c;便于求解全局最优解&#xff0c;始终…

有公网IP,如何设置端口映射实现访问?

很多中小型公司或个人会根据自身需求自建服务器&#xff0c;或者将自己内网的服务、应用发布到外网&#xff0c;实现异地访问&#xff0c;如远程桌面、网站、数据库、公司的管理系统、FTP、管家婆、监控系统等等。 没接触过的人可能会觉得这个很难&#xff0c;实际上使用快解析…