在 Java 桌面应用程序中使用 Windows EcoQoS API

在 Java 桌面应用程序中使用 Windows EcoQoS API

为了提高性能,PBH 通过 Java 21 的虚拟线程,极大的提升了并发能力。通过优化并发(以及添加缓存),从 5000ms+ 的检查耗时一度降到 100-500ms。对于服务器场景,这当然是极好的。但 PBH 主要运行在用户的终端设备上,这就引发了一个问题...

从 5.1.0 开始,PeerBanHelper 引入了 EcoMode。

简单介绍下:PBH 是一个 BT 反吸血软件,它通过每隔一段特定时间 WebAPI 连接 BT 下载器反吸血。然后执行规则和过滤器,并在检查完毕后根据结果做出不同的动作。

为了提高性能,PBH 通过 Java 21 的虚拟线程,极大的提升了并发能力。通过优化并发(以及添加缓存),从 5000ms+ 的检查耗时一度降到 100-500ms。对于服务器场景,这当然是极好的。但 PBH 主要运行在用户的终端设备上,这就引发了一个问题—— CPU 在每次规则检查时都会睿频,但是由于每次规则检查持续过程都很短,因此 CPU 总是睿了一下就没活干了。

更加雪上加霜的是,由于检查是定时进行(通常是 3000 毫秒),每隔 3 秒就忽悠 CPU 动一下,时间久了,不但电脑热了,用户还成为了国家电网合作伙伴。

检查耗时久了,封禁不及时;检查耗时短了,又耗电。如果有办法把 PBH 的任务尽快完成的同时,又可以避免 CPU 睿频那就太好了。

从能源之星得到启发

在思考这个问题的时候,我突然想起来:我使用的是笔记本,为了上课的时候尽可能减少耗电,提高电池续航能力,避免上课上一半没电的尴尬情况,我装了个名为 “能源 X” 的软件。在过去的一个学期里,这款软件表现良好,平均延长了超过 30-60 分钟的电池续航。

它不需要第三方应用适配就能使用,那么原理是什么呢?切换到帮助页,贴心的作者已经在这里详细的阐述了软件的工作原理:

能源之星X 利用 Windows 11 的 EcoQos API(即“效率模式”)来限制后台应用的资源占用,从而提高电池续航和散热表现。它不会限制前台应用,以确保用户体验。

此应用是开源程序 EnergyStar 的图形界面版本,使用 Windows App SDK (WinUI 3) 开发。

在这篇引用的博文中,微软介绍到:

Windows 会为进程和线程关联服务质量 (QoS),以显示性能和能效之间的平衡。从 Windows 10 Insider Preview Build 21359 开始,Windows 包含了一个名为 EcoQoS 的新服务质量级别。这种新的 QoS 级别对于没有显著性能或延迟要求的工作负载非常有价值,可使它们始终以节能方式运行。开发人员可以调用 API 来明确选择将其进程和线程识别为 EcoQoS,Windows 会处理其余事宜。Windows 将以此为提示,自动将这些工作安排给最高效的处理器,并将处理器配置为以最高效的时钟速度运行。EcoQoS 非常适合后台服务、更新程序、同步引擎、索引服务等注重能效的服务。


对 EcoQoS 的初步评估显示,CPU 功耗最多可降低 90%,而完成相同工作所需的 CPU 能耗不到一半。并且可以提供:

1) 能源效率和可持续性

2) 降低热量和风扇噪音

3) 减少功率/热节流--提高并发工作负载的性能

PBH 恰恰就是那种 “后台服务” 并且 “没有显著性能或延迟要求” 的工作负载。它俩简直是天生一对。

造轮子之前先看看有没有现成的

截至本文发布时,以 “EcoMode”、"EcoQoS" 以及 “Efficiency Mode” 关键词在 GitHub 上搜索,对于 Java 来说没有任何现成的类库。

唯一有些擦边的是 OpenHFT/Java-Thread-Affinity,但是仅仅是调整线程优先级,并不能达到我想要的避免睿频和降低发热的效果。

查看 EnergyStar 的代码,得知 C# 下是调用的系统 DLL 方法实现的。回到官方文档中,可以看到微软给出了两个方法:

SetProcessInformation 和 SetThreadInformation 函数分别提供了一种在特定进程或单个线程上选择 EcoQoS 的方法。开发人员可以调用这些函数并指定使用 PowerThrottling,使进程/线程被识别为 EcoQoS 并更高效地执行。

彳亍,这就是当你向人烟稀少或者无人领域探索时会遇到的事情。用 JNI 造轮子是不可避免的事情。

ChatGPT 助我编译

尽管为了高考,确实被迫学习了 C 语言。要直接调用 C 接口,需要使用到 JNI 技术,也就需要我在 C 上面搓个 DLL 出来。但对于我这种 Javaer,直接和系统通信还是有点难为我了。但在 AI 的时代,ChatGPT 总是我的良师益友。最重要的是在凌晨四点的时候问他问题他不会烦也不会生气(

让 ChatGPT 给出有帮助的答案的重要一点是要让 ChatGPT 明白 “我要的是 X 而不是相似的 Y”,要最简单的做到这一点,是直接把微软的文档丢给它。一来一回的对话,得到了如下可编译为 DLL 的 C 代码:

#include <jni.h>
#include <windows.h>
#include <processthreadsapi.h>
#include <string>

extern "C"
{
    // 笔者注:要使一个程序真正进入 “效率模式”,需要设置两个属性:进程优先级需要设置为 “IDLE_PRIORITY_CLASS”,PowerThrottling 需要设置为PROCESS_POWER_THROTTLING_EXECUTION_SPEED;至于为什么要设置两个,可能这就是巨硬吧
    JNIEXPORT jstring JNICALL Java_com_ghostchu_lib_jni_EcoMode_setEcoMode(JNIEnv *env, jobject obj, jobject enable)
    {
        PROCESS_POWER_THROTTLING_STATE PowerThrottling;
        RtlZeroMemory(&PowerThrottling, sizeof(PowerThrottling));
        PowerThrottling.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;

        //
        // EcoQoS
        // Turn EXECUTION_SPEED throttling on.
        // ControlMask selects the mechanism and StateMask declares which mechanism should be on or off.
        //

        PowerThrottling.ControlMask = enable ? PROCESS_POWER_THROTTLING_EXECUTION_SPEED : NULL;
        PowerThrottling.StateMask = enable ? PROCESS_POWER_THROTTLING_EXECUTION_SPEED : NULL;

        std::string message;

        if (!SetProcessInformation(GetCurrentProcess(),
                                   ProcessPowerThrottling,
                                   &PowerThrottling,
                                   sizeof(PowerThrottling)))
        {
            DWORD error = GetLastError();
            message = "SetProcessInformation failed with error: " + std::to_string(error);
            return env->NewStringUTF(message.c_str());
        }

        if (!SetPriorityClass(GetCurrentProcess(), enable ? IDLE_PRIORITY_CLASS : NORMAL_PRIORITY_CLASS))
        {
            DWORD error = GetLastError();
            message = "SetPriorityClass failed with error: " + std::to_string(error);
        }
        else
        {
            message = "SUCCESS";
        }
        // 将 C++ 字符串转换为 Java 字符串并返回
        return env->NewStringUTF(message.c_str());
    }
    // 未使用,但我觉得挺有用就留在这里,可以改变进程的优先级 setPriority(-1/0/1/2) 对应 低、普通、高、实时 ;)
    JNIEXPORT jint JNICALL Java_com_ghostchu_lib_jni_ProcessPriority_setPriority(JNIEnv *env, jclass cls, jint priority)
    {
        HANDLE hProcess = GetCurrentProcess();
        DWORD dwPriorityClass;

        switch (priority)
        {
        case -1:
            dwPriorityClass = IDLE_PRIORITY_CLASS;
            break;
        case 0:
            dwPriorityClass = NORMAL_PRIORITY_CLASS;
            break;
        case 1:
            dwPriorityClass = HIGH_PRIORITY_CLASS;
            break;
        case 2:
            dwPriorityClass = REALTIME_PRIORITY_CLASS;
            break;
        default:
            return -1; // Invalid priority
        }

        if (SetPriorityClass(hProcess, dwPriorityClass))
        {
            return 0; // Success
        }
        else
        {
            return -1; // Failure
        }
    }
}

要编译它,首先要准备 C++ 编译工具链。这倒不是很困难,打开 Visual Studio Installer,打勾 “使用 C++ 的桌面开发”,然后在右边选择适合的 VC 版本。

通常来说,VC 版本越低,就可以在更多的机器上运行。本文的代码笔者选择 MSVC v140 - VS 2015 C++ 生成工具(VC 2015),以支持到 Windows 7 操作系统。

安装完成后,在开始菜单中找到 "Visual Studio 2015" 文件夹,在其中选择需要编译到的目标平台的命令提示符打开。

笔者的软件主要适配 x86_64 (也就是人们常说的 x86/amd64 平台)和 ARM64 平台,因此分别选择了 “VS2015 x86 64 兼容工具命令提示符” 和 “VS2015 x64 ARM 兼容工具命令提示符”。

运行如下命令(需要提前配置好 JAVA HOME 的环境变量),就能拿到 DLL 啦:

cl /LD /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" 源文件.cpp /link /out:输出文件.dll

JNI 接入,发动 EcoQoS 的魔法

在上文中,声明的 C 函数名为 Java_com_ghostchu_lib_jni_EcoMode_setEcoMode。这个函数名可不是乱起的,它由 “Java_包名_类名_方法名” 分别组成。这样我们只要声明一个相同位置的 native 修饰的方法,就可以调用这个 C 函数啦。

建立一个名为 com.ghostchu.lib.jni 的包,并创建一个名为 EcoMode 的类,把代码填充一下:

package com.ghostchu.lib.jni;

import com.ghostchu.peerbanhelper.Main;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Locale;

@Slf4j
public class EcoMode {
    /**
     * 是否为当前 Java 进程启用 EcoMode
     * @param enable 是否启用
     * @return 操作是否成功
     */
    public static boolean ecoMode(boolean enable) {
        String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
        if (!os.startsWith("win")) {
            throw new IllegalStateException("只有 Windows 操作系统支持 EcoMode API");
        }
        String arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT);
        try {
            File tmpFile = Files.createTempFile("pbh-jni-lib", ".dll").toFile(); // 创建临时文件
            tmpFile.deleteOnExit(); // 程序退出时,删除创建的临时文件
            if (arch.contains("aarch64")) { // CPU 架构检查,aarch64 = ARM64 处理器
                Files.copy(Main.class.getResourceAsStream("/native/windows/ghost-common-jni_vc2015_aarch64.dll"), tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 从 JAR 中释放 DLL 文件
            } else {
                Files.copy(Main.class.getResourceAsStream("/native/windows/ghost-common-jni_vc2015_amd64.dll"), tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 从 JAR 中释放 DLL 文件
            }
            System.load(tmpFile.getAbsolutePath()); // 加载 DLL 文件
        } catch (IOException e) {
            log.error("Unable load JNI native libraries", e);
        }
        try {
            String data = setEcoMode(enable);
            return data.equals("SUCCESS"); // 判断是否成功
        } catch (Throwable e) {
            return false;
        }
    }

    private native static String setEcoMode(boolean enable); // 魔法于此,调用这个 static native 方法时,就会执行 C 函数
}

最后找个位置调用一下这个新鲜出炉的方法,就可以啦!

@Component
@Slf4j
public class WindowsEcoQosAPI {
    private final YamlConfiguration config;

    public WindowsEcoQosAPI() {
        this.config = Main.getMainConfig();
        if (this.config.getBoolean("performance.windows-ecoqos-api")) {
            installEcoQosApi();
        }
    }

    private void installEcoQosApi() {
        String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
        if (os.startsWith("win")) {
            if (EcoMode.ecoMode(true)) {
                log.info(tlUI(Lang.IN_ECOMODE_DESCRIPTION));
                ExchangeMap.GUI_DISPLAY_FLAGS.add(new ExchangeMap.DisplayFlag(10, tlUI(Lang.IN_ECOMODE_SHORT)));
            }
        }
    }
}

除特殊说明以外,本站原创内容采用 知识共享 署名-非商业性使用 4.0 (CC BY-NC 4.0) 许可。转载时请注明来源,以及原文链接。
Comment