从 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)));
}
}
}
}