Skip to content

Commit

Permalink
Switch to percentage-based limits
Browse files Browse the repository at this point in the history
earlyoom used to calculate the memory and swap limits to absolute
kilobyte values on startup, even if percentages were given by the
user. This model breaks down once swap space is added dynamically on
runtime.

Switch to always using limits as a percentage of total memory
and swap. If the user passes kilobytes values (-S and -M), these
are converted to a percentage at startup.

Fixes #62

Low memory output now looks like this:

  Low memory! mem avail: 228 of 7836 MiB (2) % <= min 10 %, swap free: 8 of 99 MiB (8 %) <= min 10 %
  Killing process: tail, pid: 14259, badness: 629, VmRSS: 1246 MiB

Fixes #66
Fixes #60
  • Loading branch information
rfjakob committed Jul 8, 2018
1 parent 09f0826 commit 88e5890
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 44 deletions.
35 changes: 12 additions & 23 deletions main.c
Original file line number Diff line number Diff line change
Expand Up @@ -210,29 +210,27 @@ int main(int argc, char* argv[])
struct meminfo m = parse_meminfo();

if (mem_min) {
mem_min_percent = 100 * mem_min / m.MemTotal;
mem_min_percent = 100 * mem_min / m.MemTotalKiB;
} else {
if (!mem_min_percent) {
mem_min_percent = 10;
}
mem_min = m.MemTotal * mem_min_percent / 100;
}

if (swap_min) {
if (m.SwapTotal > 0) {
swap_min_percent = 100 * swap_min / m.SwapTotal;
if (m.SwapTotalKiB > 0) {
swap_min_percent = 100 * swap_min / m.SwapTotalKiB;
}
} else {
if (!swap_min_percent) {
swap_min_percent = 10;
}
swap_min = m.SwapTotal * swap_min_percent / 100;
}

fprintf(stderr, "mem total: %lu MiB, min: %lu MiB (%d %%)\n",
m.MemTotal / 1024, mem_min / 1024, mem_min_percent);
fprintf(stderr, "swap total: %lu MiB, min: %lu MiB (%d %%)\n",
m.SwapTotal / 1024, swap_min / 1024, swap_min_percent);
fprintf(stderr, "mem total: %4d MiB, min: %2d %%\n",
m.MemTotalMiB, mem_min_percent);
fprintf(stderr, "swap total: %4d MiB, min: %2d %%\n",
m.SwapTotalMiB, swap_min_percent);

if (notif_command)
fprintf(stderr, "notifications enabled using command: %s\n", notif_command);
Expand All @@ -259,10 +257,9 @@ int main(int argc, char* argv[])
while (1) {
m = parse_meminfo();

if (m.MemAvailable <= mem_min && m.SwapFree <= swap_min) {
print_mem_stats(stderr, m);
fprintf(stderr, "Out of memory! mem min: %lu MiB, swap min: %lu MiB\n",
mem_min / 1024, swap_min / 1024);
if (m.MemAvailablePercent <= mem_min_percent && m.SwapFreePercent <= swap_min_percent) {
fprintf(stderr, "Low memory! mem avail: %d of %d MiB (%d) %% <= min %d %%, swap free: %d of %d MiB (%d %%) <= min %d %%\n",
m.MemAvailableMiB, m.MemTotalMiB, m.MemAvailablePercent, mem_min_percent, m.SwapFreeMiB, m.SwapTotalMiB, m.SwapFreePercent, swap_min_percent);
handle_oom(procdir, 9, kernel_oom_killer, ignore_oom_score_adj,
notif_command, prefer_regex, avoid_regex);
oom_cnt++;
Expand All @@ -281,17 +278,9 @@ int main(int argc, char* argv[])
*/
void print_mem_stats(FILE* out_fd, const struct meminfo m)
{
long mem_mib = m.MemAvailable / 1024;
long mem_percent = m.MemAvailable * 100 / m.MemTotal;
long swap_mib = m.SwapFree / 1024;
long swap_percent = 0;

if (m.SwapTotal > 0)
swap_percent = m.SwapFree * 100 / m.SwapTotal;

fprintf(out_fd,
"mem avail: %ld MiB (%ld %%), swap free: %ld MiB (%ld %%)\n",
mem_mib, mem_percent, swap_mib, swap_percent);
"mem avail: %4d of %4d MiB (%2d %%), swap free: %4d of %4d MiB (%2d %%)\n",
m.MemAvailableMiB, m.MemTotalMiB, m.MemAvailablePercent, m.SwapFreeMiB, m.SwapTotalMiB, m.SwapFreePercent);
}

int set_oom_score_adj(int oom_score_adj)
Expand Down
26 changes: 20 additions & 6 deletions meminfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,33 @@ struct meminfo parse_meminfo()
}
buf[len] = 0; // Make sure buf is zero-terminated

m.MemTotal = get_entry_fatal("MemTotal:", buf);
m.SwapTotal = get_entry_fatal("SwapTotal:", buf);
m.SwapFree = get_entry_fatal("SwapFree:", buf);
m.MemTotalKiB = get_entry_fatal("MemTotal:", buf);
m.SwapTotalKiB = get_entry_fatal("SwapTotal:", buf);
long SwapFree = get_entry_fatal("SwapFree:", buf);

m.MemAvailable = get_entry("MemAvailable:", buf);
if (m.MemAvailable == -1) {
m.MemAvailable = available_guesstimate(buf);
long MemAvailable = get_entry("MemAvailable:", buf);
if (MemAvailable == -1) {
MemAvailable = available_guesstimate(buf);
if (guesstimate_warned == 0) {
fprintf(stderr, "Warning: Your kernel does not provide MemAvailable data (needs 3.14+)\n"
" Falling back to guesstimate\n");
guesstimate_warned = 1;
}
}

// Calculate percentages
m.MemAvailablePercent = MemAvailable * 100 / m.MemTotalKiB;
if (m.SwapTotalKiB > 0) {
m.SwapFreePercent = SwapFree * 100 / m.SwapTotalKiB;
} else {
m.SwapFreePercent = 0;
}

// Convert kiB to MiB
m.MemTotalMiB = m.MemTotalKiB / 1024;
m.MemAvailableMiB = MemAvailable / 1024;
m.SwapTotalMiB = m.SwapTotalKiB / 1024;
m.SwapFreeMiB = SwapFree / 1024;

return m;
}
15 changes: 10 additions & 5 deletions meminfo.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
/* SPDX-License-Identifier: MIT */

struct meminfo {
long MemTotal;
long MemAvailable;
long SwapTotal;
long SwapFree;
/* -1 means no data available */
// Values from /proc/meminfo, in KiB or converted to MiB.
long MemTotalKiB;
int MemTotalMiB;
int MemAvailableMiB; // -1 means no data available
int SwapTotalMiB;
long SwapTotalKiB;
int SwapFreeMiB;
// Calculated percentages
int MemAvailablePercent; // percent of total memory that is available
int SwapFreePercent; // percent of total swap that is free
};

struct meminfo parse_meminfo();
35 changes: 26 additions & 9 deletions tests/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,49 @@ func TestCli(t *testing.T) {
{args: []string{"-p"}, code: -1, stdoutContains: memReport},
{args: []string{"-v"}, code: 0, stderrContains: "earlyoom v", stdoutEmpty: true},
{args: []string{"-d"}, code: -1, stdoutContains: "^ new victim (higher badness)"},
{args: []string{"-m", "1"}, code: -1, stderrContains: "(1 %)", stdoutContains: memReport},
{args: []string{"-m", "1"}, code: -1, stderrContains: "1 %", stdoutContains: memReport},
{args: []string{"-m", "0"}, code: 15, stderrContains: "Invalid percentage", stdoutEmpty: true},
{args: []string{"-s", "2"}, code: -1, stderrContains: "(2 %)", stdoutContains: memReport},
{args: []string{"-s", "2"}, code: -1, stderrContains: "2 %", stdoutContains: memReport},
{args: []string{"-s", "0"}, code: 16, stderrContains: "Invalid percentage", stdoutEmpty: true},
{args: []string{"-M", "1024"}, code: -1, stderrContains: "min: 1 MiB", stdoutContains: memReport},
{args: []string{"-S", "2048"}, code: -1, stderrContains: "min: 2 MiB", stdoutContains: memReport},
// {args: []string{"-M", "1024"}, code: -1, stderrContains: "min: 1 MiB", stdoutContains: memReport},
// {args: []string{"-S", "2048"}, code: -1, stderrContains: "min: 2 MiB", stdoutContains: memReport},
{args: []string{"-r", "0"}, code: -1, stderrContains: startupMsg, stdoutEmpty: true},
}

for i, tc := range testcases {
t.Logf("Testcase #%d: earlyoom %s", i, strings.Join(tc.args, " "))
pass := true
res := runEarlyoom(t, tc.args...)
t.Logf("Testcase #%d: exit code = %d", i, res.code)
if res.code != tc.code {
t.Errorf("wrong exit code: have=%d want=%d", res.code, tc.code)
pass = false
}
if tc.stdoutEmpty && res.stdout != "" {
t.Errorf("stdout should be empty, but contains:\n%s", res.stdout)
t.Errorf("stdout should be empty but is not")
pass = false
}
if !strings.Contains(res.stdout, tc.stdoutContains) {
t.Errorf("stdout should contain %q, but does not:\n%s", tc.stdoutContains, res.stdout)
t.Errorf("stdout should contain %q, but does not", tc.stdoutContains)
pass = false
}
if tc.stderrEmpty && res.stderr != "" {
t.Errorf("stderr should be empty, but contains:\n%s", res.stderr)
t.Errorf("stderr should be empty, but is not")
pass = false
}
if !strings.Contains(res.stderr, tc.stderrContains) {
t.Errorf("stderr should contain %q, but does not:\n%s", tc.stderrContains, res.stderr)
t.Errorf("stderr should contain %q, but does not", tc.stderrContains)
pass = false
}
if !pass {
const empty = "(empty)"
if res.stderr == "" {
res.stderr = empty
}
if res.stdout == "" {
res.stdout = empty
}
t.Logf("stderr:\n%s", res.stderr)
t.Logf("stdout:\n%s", res.stdout)
}
}
}
1 change: 0 additions & 1 deletion tests/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ func runEarlyoom(t *testing.T, args ...string) exitVals {
var timer *time.Timer
timer = time.AfterFunc(100*time.Millisecond, func() {
timer.Stop()
t.Logf("killing process after timeout")
cmd.Process.Kill()
})
err := cmd.Run()
Expand Down

0 comments on commit 88e5890

Please sign in to comment.