diff --git a/.github/release-nest-v3 b/.github/release-nest-v3 index 56a6051..00750ed 100644 --- a/.github/release-nest-v3 +++ b/.github/release-nest-v3 @@ -1 +1 @@ -1 \ No newline at end of file +3 diff --git a/Makefile-v3 b/Makefile-v3 deleted file mode 100644 index 5608ce4..0000000 --- a/Makefile-v3 +++ /dev/null @@ -1,7 +0,0 @@ -all: - true - -install: - mkdir -p $(DESTDIR)/usr/bin/ - GOAMD=v3 go build -ldflags="-s -w" -o $(DESTDIR)/usr/bin/falcon -buildvcs=false - chmod 755 $(DESTDIR)/usr/bin/falcon diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..d927ea6 --- /dev/null +++ b/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "falcon", + .root_source_file = .{ .cwd_relative = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(exe); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..2265261 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "falcon", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.1.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/debian/changelog b/debian/changelog index 3c2401e..c289a5d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +falcon (1.0.4-101pika1) pika; urgency=low + + * Zig version + configs + + -- Ward Nakchbandi Sat, 10 Dec 2022 13:48:00 +0300 + falcon (1.0.3-101pika1) pika; urgency=low * Fix international diff --git a/debian/control b/debian/control index 0cfb97e..36360da 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: optional Maintainer: ferreo Rules-Requires-Root: no Build-Depends: - debhelper-compat (= 13), golang-go + debhelper-compat (= 13), zig-nightly Standards-Version: 4.6.1 Homepage: https://pika-os.com diff --git a/debian/rules b/debian/rules index 1c09589..6bbb831 100755 --- a/debian/rules +++ b/debian/rules @@ -8,3 +8,13 @@ override_dh_dwz: %: dh $@ + +override_dh_install: + dh_install + mkdir -p debian/falcon/usr/bin/ + cp -vf falcon debian/falcon/usr/bin/ + chmod 755 debian/falcon/usr/bin/falcon + chmod +x debian/falcon/usr/bin/falcon + +override_dh_auto_build: + zig build-exe src/main.zig -O ReleaseFast -mcpu x86_64_v3 --name falcon \ No newline at end of file diff --git a/go.mod b/go.mod deleted file mode 100644 index 5f724a8..0000000 --- a/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module falcon - -go 1.22 - -require golang.org/x/sync v0.8.0 diff --git a/go.sum b/go.sum deleted file mode 100644 index e584c1b..0000000 --- a/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= diff --git a/main.go b/main.go deleted file mode 100644 index 56c752e..0000000 --- a/main.go +++ /dev/null @@ -1,238 +0,0 @@ -package main - -import ( - "bufio" - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "sort" - "strconv" - "strings" - - "golang.org/x/sync/errgroup" -) - -type CPUInfo struct { - CPU int `json:"cpu"` - MaxMHz float64 `json:"maxmhz"` -} - -type LSCPUOutput struct { - CPUs []CPUInfo `json:"cpus"` -} - -type CoreFreq struct { - Core int - Freq int -} - -func main() { - args := os.Args[1:] - if len(args) == 0 { - fmt.Println("Please provide a command to run") - os.Exit(1) - } - - eg, ctx := errgroup.WithContext(context.Background()) - canPerf, cores, err := getBestCores() - if err != nil { - eg.Go(func() error { - return runCommand(ctx, false, []int{}, args) - }) - err = eg.Wait() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - os.Exit(0) - } - - eg.Go(func() error { - return runCommand(ctx, canPerf, cores, args) - }) - err = eg.Wait() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - os.Exit(0) -} - -func runCommand(ctx context.Context, canPerf bool, cores []int, args []string) error { - commandToRun := make([]string, 0) - if canPerf { - commandToRun = append(commandToRun, "powerprofilesctl", "launch", "-p", "performance") - } - - if len(cores) > 0 { - coreVals := make([]string, len(cores)) - for i, core := range cores { - coreVals[i] = fmt.Sprint(core) - } - - commandToRun = append(commandToRun, "taskset", "-c", strings.Join(coreVals, ",")) - } - - commandToRun = append(commandToRun, args...) - - cmd := exec.CommandContext(ctx, commandToRun[0], commandToRun[1:]...) - cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - return cmd.Run() -} - -func getBestCores() (bool, []int, error) { - perfMode, amdPrefCores, amdFlag, highestCores, err := GetPPStatusAndHighestCores() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if !amdFlag { - return perfMode, highestCores, nil - } - - numAmdCores := len(amdPrefCores) - switch numAmdCores { - case 32: - return perfMode, getHighestCores(16, amdPrefCores), nil - case 24: - return perfMode, getHighestCores(12, amdPrefCores), nil - case 16: - return perfMode, getHighestCores(8, amdPrefCores), nil - default: - return perfMode, getHighestCores(numAmdCores, amdPrefCores), nil - } -} - -func getHighestCores(n int, coreFreqs []CoreFreq) []int { - if n <= 0 { - return []int{} - } - - sort.Slice(coreFreqs, func(i, j int) bool { - return coreFreqs[i].Freq > coreFreqs[j].Freq - }) - - result := make([]int, 0, n) - for i := 0; i < n && i < len(coreFreqs); i++ { - result = append(result, coreFreqs[i].Core) - } - sort.Stable(sort.IntSlice(result)) - - return result -} - -func GetPPStatusAndHighestCores() (bool, []CoreFreq, bool, []int, error) { - var ( - performanceMode bool - amdPstateFreqs []CoreFreq - amdFlag bool - highestCores []int - ) - - g, ctx := errgroup.WithContext(context.Background()) - g.Go(func() error { - cmd := exec.CommandContext(ctx, "powerprofilesctl", "list") - output, err := cmd.Output() - if err != nil { - performanceMode = false - return nil - } - - performanceMode = strings.Contains(string(output), "performance") - return nil - }) - - g.Go(func() error { - pattern := "/sys/devices/system/cpu/cpu*/cpufreq/amd_pstate_prefcore_ranking" - matches, err := filepath.Glob(pattern) - if err != nil { - return fmt.Errorf("failed to glob AMD pstate files: %w", err) - } - - if len(matches) > 0 { - amdFlag = true - } - - for _, match := range matches { - file, err := os.Open(match) - if err != nil { - return fmt.Errorf("failed to open file %s: %w", match, err) - } - defer file.Close() - - parts := strings.Split(match, "/") - cpuPart := parts[len(parts)-3] - coreNum, err := strconv.Atoi(strings.TrimPrefix(cpuPart, "cpu")) - if err != nil { - return fmt.Errorf("failed to parse core number from path %s: %w", match, err) - } - - scanner := bufio.NewScanner(file) - if scanner.Scan() { - value, err := strconv.Atoi(strings.TrimSpace(scanner.Text())) - if err != nil { - return fmt.Errorf("failed to parse value from file %s: %w", match, err) - } - - amdPstateFreqs = append(amdPstateFreqs, CoreFreq{Core: coreNum, Freq: value}) - } - - if err := scanner.Err(); err != nil { - return fmt.Errorf("error reading file %s: %w", match, err) - } - } - return nil - }) - - g.Go(func() error { - cmd := exec.CommandContext(ctx, "lscpu", "-e", "-J") - cmd.Env = append(cmd.Env, "LANG=en_GB.utf8") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to run lscpu command: %w", err) - } - - var lscpuOutput LSCPUOutput - if err := json.Unmarshal(output, &lscpuOutput); err != nil { - return fmt.Errorf("failed to unmarshal lscpu output: %w", err) - } - - sort.Slice(lscpuOutput.CPUs, func(i, j int) bool { - return lscpuOutput.CPUs[i].MaxMHz > lscpuOutput.CPUs[j].MaxMHz - }) - - if len(lscpuOutput.CPUs) > 0 { - topMHz := lscpuOutput.CPUs[0].MaxMHz - secondMHz := topMHz - - for _, cpu := range lscpuOutput.CPUs { - if cpu.MaxMHz < topMHz { - secondMHz = cpu.MaxMHz - break - } - } - - for _, cpu := range lscpuOutput.CPUs { - if cpu.MaxMHz == topMHz || cpu.MaxMHz == secondMHz { - highestCores = append(highestCores, cpu.CPU) - } - } - } - - return nil - }) - - if err := g.Wait(); err != nil { - return false, nil, false, nil, err - } - - return performanceMode, amdPstateFreqs, amdFlag, highestCores, nil -} diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..59247f0 --- /dev/null +++ b/src/config.zig @@ -0,0 +1,72 @@ +const std = @import("std"); +const fs = std.fs; +const json = std.json; + +pub const Config = struct { + force_lscpu: bool = false, + thread_count: usize = 16, + override_thread_count: bool = false, + enable_performance_mode: bool = true, + disable_taskset: bool = false, + custom_taskset: []const u8 = "", + lscpu_core_strategy: enum { + HighestFreq, + Sequential, + } = .HighestFreq, + + pub fn load(allocator: std.mem.Allocator) !Config { + const home = std.posix.getenv("HOME") orelse return Config{}; + + var path_buf: [fs.max_path_bytes]u8 = undefined; + const config_path = try std.fmt.bufPrint( + &path_buf, + "{s}/.config/falcon/config.json", + .{home}, + ); + + const file = fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) { + error.FileNotFound => { + const default_config = Config{}; + try default_config.save(); + return default_config; + }, + else => return err, + }; + defer file.close(); + + const content = try file.readToEndAlloc(allocator, std.math.maxInt(usize)); + defer allocator.free(content); + + var parsed = try json.parseFromSlice(Config, allocator, content, .{}); + defer parsed.deinit(); + + return parsed.value; + } + + pub fn save(self: Config) !void { + const home = std.posix.getenv("HOME") orelse return error.NoHomeDir; + + var path_buf: [fs.max_path_bytes]u8 = undefined; + const config_dir = try std.fmt.bufPrint( + &path_buf, + "{s}/.config/falcon", + .{home}, + ); + + fs.makeDirAbsolute(config_dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + const config_path = try std.fmt.bufPrint( + &path_buf, + "{s}/config.json", + .{config_dir}, + ); + + const file = try fs.createFileAbsolute(config_path, .{}); + defer file.close(); + + try json.stringify(self, .{ .whitespace = .indent_2 }, file.writer()); + } +}; diff --git a/src/core_detection/amd.zig b/src/core_detection/amd.zig new file mode 100644 index 0000000..5218c92 --- /dev/null +++ b/src/core_detection/amd.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const types = @import("../types.zig"); + +pub const AMDDetector = struct { + allocator: std.mem.Allocator, + + const Self = @This(); + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .allocator = allocator, + }; + } + + pub fn detect(self: *Self) !?[]const types.CoreFreq { + var cpu_dir = try std.fs.cwd().openDir("/sys/devices/system/cpu", .{ .iterate = true }); + defer cpu_dir.close(); + + var cores = std.ArrayList(types.CoreFreq).init(self.allocator); + + var it = cpu_dir.iterate(); + while (try it.next()) |entry| { + if (!std.mem.startsWith(u8, entry.name, "cpu")) continue; + if (std.mem.eql(u8, entry.name, "cpuidle") or std.mem.eql(u8, entry.name, "cpufreq")) continue; + + const ranking_path = try std.fmt.allocPrint(self.allocator, "{s}/cpufreq/amd_pstate_prefcore_ranking", .{entry.name}); + + const file = cpu_dir.openFile(ranking_path, .{}) catch continue; + defer file.close(); + + const core_num = try std.fmt.parseInt(i32, entry.name[3..], 10); + + var buf: [64]u8 = undefined; + const bytes_read = try file.readAll(&buf); + const value = try std.fmt.parseInt(i32, std.mem.trim(u8, buf[0..bytes_read], "\n"), 10); + + try cores.append(.{ .core = core_num, .freq = value }); + } + + return if (cores.items.len > 0) + try cores.toOwnedSlice() + else + null; + } +}; diff --git a/src/core_detection/lscpu.zig b/src/core_detection/lscpu.zig new file mode 100644 index 0000000..044022d --- /dev/null +++ b/src/core_detection/lscpu.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +const types = @import("../types.zig"); +const Child = std.process.Child; +const ArrayList = std.ArrayList; + +pub const LSCPUDetector = struct { + allocator: std.mem.Allocator, + + const Self = @This(); + + pub fn init(allocator: std.mem.Allocator) Self { + return .{ + .allocator = allocator, + }; + } + + pub fn detect(self: *Self) ![]const i32 { + var highest_cores = ArrayList(i32).init(self.allocator); + + var child = Child.init(&[_][]const u8{ "lscpu", "-e", "-J" }, self.allocator); + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Inherit; + + try child.spawn(); + + if (child.stdout) |stdout| { + var buffer = ArrayList(u8).init(self.allocator); + + try stdout.reader().readAllArrayList(&buffer, std.math.maxInt(usize)); + + if (buffer.items.len > 0) { + var parsed = try std.json.parseFromSlice( + types.LsCpuOutput, + self.allocator, + buffer.items, + .{}, + ); + defer parsed.deinit(); + + if (parsed.value.cpus.len > 0) { + const top_mhz = parsed.value.cpus[0].maxmhz; + var second_mhz = top_mhz; + + for (parsed.value.cpus) |cpu| { + if (cpu.maxmhz < top_mhz) { + second_mhz = cpu.maxmhz; + break; + } + } + + for (parsed.value.cpus) |cpu| { + if (cpu.maxmhz == top_mhz or cpu.maxmhz == second_mhz) { + try highest_cores.append(cpu.cpu); + } + } + } + } + } + + _ = try child.wait(); + return try highest_cores.toOwnedSlice(); + } +}; diff --git a/src/core_detection/mod.zig b/src/core_detection/mod.zig new file mode 100644 index 0000000..6041c26 --- /dev/null +++ b/src/core_detection/mod.zig @@ -0,0 +1,84 @@ +const std = @import("std"); +const types = @import("../types.zig"); +const AMDDetector = @import("amd.zig").AMDDetector; +const LSCPUDetector = @import("lscpu.zig").LSCPUDetector; +const Config = @import("../config.zig").Config; + +pub const CoreDetector = struct { + allocator: std.mem.Allocator, + amd: AMDDetector, + lscpu: LSCPUDetector, + config: Config, + + const Self = @This(); + + pub fn init(allocator: std.mem.Allocator, config: Config) Self { + return .{ + .allocator = allocator, + .amd = AMDDetector.init(allocator), + .lscpu = LSCPUDetector.init(allocator), + .config = config, + }; + } + + pub fn detect(self: *Self) !types.DetectionResult { + if (!self.config.force_lscpu) { + if (try self.amd.detect()) |amd_cores| { + const num_cores = if (self.config.override_thread_count) + self.config.thread_count + else switch (amd_cores.len) { + 32 => 16, + 24 => 12, + 16 => 8, + else => amd_cores.len, + }; + + const cores = try self.getHighestCores(num_cores, amd_cores); + + return .{ + .cores = cores, + .perf_mode = self.config.enable_performance_mode, + }; + } + } + + const lscpu_cores = try self.lscpu.detect(); + var mutable_cores = try self.allocator.dupe(i32, lscpu_cores); + + if (self.config.lscpu_core_strategy == .Sequential) { + std.mem.sort(i32, mutable_cores, {}, comptime std.sort.asc(i32)); + } + + if (self.config.override_thread_count) { + mutable_cores = mutable_cores[0..@min(self.config.thread_count, mutable_cores.len)]; + } + + return .{ + .cores = mutable_cores, + .perf_mode = self.config.enable_performance_mode, + }; + } + + fn getHighestCores(self: *Self, n: usize, core_freqs: []const types.CoreFreq) ![]const i32 { + if (n <= 0) return &[_]i32{}; + + var freqs = try self.allocator.dupe(types.CoreFreq, core_freqs); + defer self.allocator.free(freqs); + + std.mem.sort(types.CoreFreq, freqs, {}, struct { + fn lessThan(_: void, a: types.CoreFreq, b: types.CoreFreq) bool { + return a.freq > b.freq; + } + }.lessThan); + + const result_len = @min(n, freqs.len); + var result = try self.allocator.alloc(i32, result_len); + + for (freqs[0..result_len], 0..) |freq, i| { + result[i] = freq.core; + } + + std.mem.sort(i32, result, {}, comptime std.sort.asc(i32)); + return result; + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..3d1e537 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const process = std.process; +const ArrayList = std.ArrayList; +const ArenaAllocator = std.heap.ArenaAllocator; +const Child = std.process.Child; + +const CoreDetector = @import("core_detection/mod.zig").CoreDetector; +const Config = @import("config.zig").Config; + +pub fn main() !void { + var arena = ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const args = try process.argsAlloc(allocator); + defer process.argsFree(allocator, args); + + if (args.len <= 1) { + std.debug.print("Please provide a command to run\n", .{}); + process.exit(1); + } + + const config = try Config.load(allocator); + var detector = CoreDetector.init(allocator, config); + const result = try detector.detect(); + try runCommand(allocator, result.perf_mode, result.cores, args[1..], config); +} + +fn runCommand(allocator: std.mem.Allocator, can_perf: bool, cores: []const i32, args: []const []const u8, config: Config) !void { + var command = ArrayList([]const u8).init(allocator); + + if (can_perf) { + try command.appendSlice(&[_][]const u8{ "powerprofilesctl", "launch", "-p", "performance" }); + } + + if (!config.disable_taskset) { + if (config.custom_taskset.len > 0) { + try command.appendSlice(&[_][]const u8{ "taskset", "-c", config.custom_taskset }); + } else if (cores.len > 0) { + try command.appendSlice(&[_][]const u8{ "taskset", "-c" }); + var cores_list = ArrayList(u8).init(allocator); + + for (cores, 0..) |core, i| { + if (i > 0) try cores_list.append(','); + const core_str = try std.fmt.allocPrint(allocator, "{d}", .{core}); + try cores_list.appendSlice(core_str); + } + + const cores_str = try cores_list.toOwnedSlice(); + try command.append(cores_str); + } + } + + try command.appendSlice(args); + + var child = Child.init(command.items, allocator); + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + child.stdin_behavior = .Inherit; + + var env_map = try std.process.getEnvMap(allocator); + child.env_map = &env_map; + + try child.spawn(); + const term = try child.wait(); + if (term.Exited != 0) { + return error.CommandFailed; + } +} diff --git a/src/types.zig b/src/types.zig new file mode 100644 index 0000000..10c6fd8 --- /dev/null +++ b/src/types.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub const CoreFreq = struct { + core: i32, + freq: i32, +}; + +pub const CpuInfo = struct { + cpu: i32, + node: i32, + socket: i32, + core: i32, + @"l1d:l1i:l2:l3": []const u8, + online: bool, + maxmhz: f64, + minmhz: f64, + mhz: f64, +}; + +pub const LsCpuOutput = struct { + cpus: []CpuInfo, +}; + +pub const DetectionResult = struct { + cores: []const i32, + perf_mode: bool, +};