Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,63 @@
# JSimul
# JSimul

JSimul 是一个基于 Java 21 的离散事件仿真框架,面向 SimPy 的语义与 API 对齐,提供事件、进程、条件组合以及资源调度等核心能力。项目强调组合优先的设计,通过小而精的原语拼装出丰富的行为,方便在 JVM 环境中重现 SimPy 的建模体验。

## 功能概览
- **事件驱动核心**:提供 `Event`、`Timeout`、`Process` 以及 `AllOf`、`AnyOf` 等组合原语,实现基于调度队列的时间推进机制。
- **资源建模**:内置 `Resource`、`PriorityResource`、`PreemptiveResource`、`Container`、`Store` 等资源类型,支持优先级、抢占、容量和过滤存取。
- **条件与回调**:支持条件事件求值、回调移除与复制等 SimPy 兼容特性,确保回调触发顺序与短路逻辑正确。
- **实时模式**:提供 `RealtimeEnvironment`,在需要时可按真实时间步进模拟。
- **异常与中断**:支持进程中断、异常传播、主动退出(`env.exit(...)`)等语义,贴合 Python 端行为。

## 性能特点
- **优先队列调度**:使用 `PriorityQueue` 与递增事件标识减少争用,确保大批量事件插入与弹出时的可预测性能。
- **对象复用友好**:事件与组合条件采用组合式拆分,避免深层继承开销,便于 JVM JIT 进行内联与逃逸分析。
- **并发安全的调度器锁**:通过轻量级的队列锁保护调度操作,保证在多生产者场景下的线程安全。
- **性能测试护栏**:仓库内的性能单测覆盖大批量超时事件与链式回调场景,帮助在回归时监测吞吐退化。

## 设计美学与哲学
- **组合优先**:倾向组合而非继承的实现方式,减少层级,强调可重用的行为片段。
- **语义一致性**:以 SimPy 的用户体验为基准,对事件语义、资源 API 和异常传播路径保持一致,降低迁移成本。
- **可测试性**:所有核心原语均配有针对功能与 determinism 的测试,新增特性也需要覆盖全面的单元测试。
- **简洁编码**:统一使用 UTF-8 编码与英文注释,减少本地化差异带来的理解成本。

## 模块与目录
- **`sim` 模块**:核心实现与测试集。
- `src/main/java/com/jsimul/core`:事件、进程、环境、条件等核心调度与组合逻辑。
- `src/main/java/com/jsimul/collections`:资源、容器、商店等集合型原语。
- `src/test/java`:与 SimPy 语义对齐的行为测试、异常传播测试、资源特性测试,以及性能验证用例。
- **`examples` 目录**:独立的示例代码,展示从“Hello World”到资源抢占、条件组合的常见建模套路,可直接以 `javac`/`java` 或 IDE 运行。

## 快速开始
1. 安装 JDK 21 与 Maven 3.9+。
2. 构建与运行测试:
```bash
mvn -pl sim test
```
3. 浏览 `examples` 目录下的示例,按需编译运行:
```bash
javac -cp sim/target/classes examples/basic/HelloEnvironment.java
java -cp sim/target/classes:examples basic.HelloEnvironment
```

## 特性清单(与 SimPy 对齐)
- 事件调度:延时、任意值触发、回调管理、状态检查。
- 进程语义:生成器式进程、主动退出、异常传递与堆栈保留。
- 条件组合:`allOf` / `anyOf` 组合、值合并、失败短路与结果透传。
- 资源系统:标准资源、优先级资源、可抢占资源、存储(过滤/优先)、容器容量约束与超时语义。
- 运行模式:离散事件模拟与实时模拟切换。

## 示例导航
- `examples/basic/HelloEnvironment.java`:最小事件调度与运行示例。
- `examples/resources/PriorityCheckout.java`:演示 `PriorityResource` 的占用与释放流程。
- `examples/conditions/ConditionalAssembly.java`:组合条件触发与结果收集示例。

## 测试与性能
- 功能测试:`sim/src/test/java` 覆盖环境调度、条件组合、资源抢占、异常传播等。
- 性能测试:`PerformanceSimulationTest` 验证大批量超时事件与链式回调的执行时间,防止明显性能回退。

## 贡献指南
- 提交前确保 `mvn -pl sim test` 通过。
- 新增代码需配套单元测试与必要文档更新。
- 遵循组合优先的实现方式,避免不必要的继承层级。

24 changes: 24 additions & 0 deletions examples/basic/HelloEnvironment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package basic;

import com.jsimul.core.Environment;
import com.jsimul.core.Timeout;

/**
* Minimal end-to-end simulation example: schedule two timeouts and run until all
* events finish.
*/
public class HelloEnvironment {

public static void main(String[] args) {
Environment env = new Environment();

Timeout first = env.timeout(2, "first");
Timeout second = env.timeout(1, "second");

first.addCallback(event -> System.out.printf("t=%.0f -> %s%n", env.now(), event.value()));
second.addCallback(event -> System.out.printf("t=%.0f -> %s%n", env.now(), event.value()));

env.run();
}
}

48 changes: 48 additions & 0 deletions examples/conditions/ConditionalAssembly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package conditions;

import com.jsimul.core.AllOf;
import com.jsimul.core.AnyOf;
import com.jsimul.core.Environment;
import com.jsimul.core.Process;
import com.jsimul.core.SimEvent;
import com.jsimul.core.Timeout;

/**
* Illustrates condition composition: wait for any supplier, then for all
* finishing tasks before shipping.
*/
public class ConditionalAssembly {

public static void main(String[] args) {
Environment env = new Environment();

SimEvent shipment = env.process(shipping(env));
AnyOf supplierArrival = env.anyOf(env.timeout(3, "supplier-A"), env.timeout(5, "supplier-B"));

supplierArrival.addCallback(event -> {
System.out.printf("t=%.0f -> first delivery: %s%n", env.now(), event.value());
env.process(assembly(env));
});

shipment.addCallback(event -> System.out.printf("t=%.0f -> shipment ready%n", env.now()));
env.run();
}

private static Process.ProcessFunction assembly(Environment env) {
return process -> {
Timeout frame = env.timeout(4, "frame");
Timeout electronics = env.timeout(2, "electronics");
AllOf finished = env.allOf(frame, electronics);
yield finished;
Comment on lines +33 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace generator-style yield with ProcessContext awaits

The example process is written with Python-like yield finished;, but Process.ProcessFunction expects you to block on events via the provided ProcessContext (e.g., ctx.await(...)). Using yield here is neither valid Java syntax nor a call to await an event, so the example will not compile or run; the same issue applies to the later yield env.timeout(10); in this class.

Useful? React with 👍 / 👎.

System.out.printf("t=%.0f -> assembly complete%n", env.now());
};
}

private static Process.ProcessFunction shipping(Environment env) {
return process -> {
yield env.timeout(10);
System.out.printf("t=%.0f -> shipping triggered%n", env.now());
};
}
}

39 changes: 39 additions & 0 deletions examples/resources/PriorityCheckout.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package resources;

import com.jsimul.collections.PriorityRequest;
import com.jsimul.collections.PriorityResource;
import com.jsimul.core.Environment;
import com.jsimul.core.Process;

/**
* Demonstrates how priority alters queue ordering for a resource checkout
* scenario.
*/
public class PriorityCheckout {

public static void main(String[] args) {
Environment env = new Environment();
PriorityResource cashier = new PriorityResource(env, 1);

env.process(customer(env, cashier, "regular", 5, 10));
env.process(customer(env, cashier, "vip", 2, 4));
env.process(customer(env, cashier, "walk-in", 3, 6));

env.run();
}

private static Process.ProcessFunction customer(Environment env, PriorityResource cashier, String name,
int priority, double serviceTime) {
return process -> {
PriorityRequest request = cashier.request(priority);
yield request;

System.out.printf("t=%.0f -> %s got service\n", env.now(), name);
yield env.timeout(serviceTime);
Comment on lines +28 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Example process uses invalid yield instead of ctx.await

This example uses bare yield statements (yield request;, yield env.timeout(serviceTime);) inside a ProcessFunction, but the process API requires awaiting events through the provided context (e.g., ctx.await(...)). As written the code is invalid Java and won’t compile/run, undermining the “runnable example” intent.

Useful? React with 👍 / 👎.


cashier.release(request);
System.out.printf("t=%.0f -> %s finished\n", env.now(), name);
};
}
}

52 changes: 52 additions & 0 deletions sim/src/test/java/com/jsimul/core/PerformanceSimulationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.jsimul.core;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

/**
* Performance-oriented tests to guard against major throughput regressions
* while staying deterministic.
*/
public class PerformanceSimulationTest {

@Test
void timeoutBurstCompletesWithinBudget() {
Environment env = new Environment();

assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
for (int i = 0; i < 20000; i++) {
env.timeout(1);
}

env.run();

assertEquals(1.0, env.now());
assertEquals(0, env.scheduledCount());
});
}

@Test
void denseCallbackChainExecutesQuickly() {
Environment env = new Environment();

assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
Event root = env.event();
AtomicInteger counter = new AtomicInteger();

for (int i = 0; i < 5000; i++) {
root.addCallback(evt -> counter.incrementAndGet());
}

root.succeed("ok");
env.run();

assertEquals(5000, counter.get());
assertEquals("ok", root.value());
});
}
}

109 changes: 109 additions & 0 deletions sim/src/test/java/com/jsimul/core/SimPyEventParityTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.jsimul.core;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

/**
* Additional parity checks derived from SimPy's event unit tests to
* validate callback handling, trigger propagation, and empty condition
* semantics.
*/
class SimPyEventParityTest {

@Test
void removingCallbacksPreventsInvocation() {
Environment env = new Environment();
Event event = env.event();

StringBuilder calls = new StringBuilder();
Event.Callback keep = e -> calls.append("keep");
Event.Callback remove = e -> calls.append("remove");

event.addCallback(keep);
event.addCallback(remove);
event.removeCallback(remove);

event.succeed("done");
env.step();

assertEquals("keep", calls.toString(), "Removed callback must not run");
}

@Test
void callbacksAddedAfterSchedulingStillRun() {
Environment env = new Environment();
Event event = env.event();

StringBuilder calls = new StringBuilder();
event.succeed("payload");
event.addCallback(e -> calls.append(e.value()));

env.step();

assertEquals("payload", calls.toString(), "Late-added callback should still fire");
assertTrue(event.isProcessed(), "Event should be processed after step");
}

@Test
void callbacksIgnoredAfterProcessing() {
Environment env = new Environment();
Event event = env.event().succeed("done");
env.step();

StringBuilder calls = new StringBuilder();
event.addCallback(e -> calls.append("late"));

// Run another harmless step to prove the callback will never execute
env.schedule(env.event().markOk(null), Event.NORMAL, 0.0);
env.step();

assertEquals("", calls.toString(), "Callbacks added post-processing must be ignored");
}

@Test
void triggerCopiesOutcomeFromOriginEvent() {
Environment env = new Environment();
Event origin = env.event().succeed("source");
env.step();

Event target = env.event();
target.trigger(origin);

env.step();

assertTrue(target.ok(), "Target should mirror origin success");
assertEquals("source", target.value(), "Target value should copy origin");
}

@Test
void emptyConditionsCompleteImmediatelyWithEmptyValue() {
Environment env = new Environment();

SimEvent all = env.allOf();
SimEvent any = env.anyOf();

env.step();
env.step();

ConditionValue allValue = (ConditionValue) all.asEvent().value();
ConditionValue anyValue = (ConditionValue) any.asEvent().value();

assertTrue(all.asEvent().ok());
assertTrue(any.asEvent().ok());
assertTrue(allValue.toMap().isEmpty(), "AllOf([]) should carry empty mapping");
assertTrue(anyValue.toMap().isEmpty(), "AnyOf([]) should carry empty mapping");
}

@Test
void runReturnsValueWhenUntilAlreadyProcessed() {
Environment env = new Environment();
Timeout event = env.timeout(1.0, "result");

env.run();
Object value = env.run(event.asEvent());

assertEquals("result", value, "run(until) should return processed event value");
assertEquals(1.0, env.now(), 1e-9);
}
}