-
Notifications
You must be signed in to change notification settings - Fork 0
/
dos.sml
executable file
·313 lines (284 loc) · 11.9 KB
/
dos.sml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
structure DOS :> DOS =
struct
structure GS = GameState
structure GA = GrowArray
exception DOS of string
(* Index into processes growarray below. *)
type pid = int
datatype dos = D of { pid : pid, gs : GS.gamestate }
datatype dosturn =
Turn of LTG.turn
(* If you can't take a turn this round, just return Can'tRun.
The operating system will take a turn for some other dominator,
internal operation, or if none can run, idle. *)
| Can'tRun
type dominator = { preview : dos -> unit,
taketurn : dos -> dosturn }
datatype process =
P of { reserved_slots : int list ref,
name : string,
priority : real ref,
parent : pid option,
dominator : dominator,
(* How many virtual cycles has this process has "run", which is
real turns / priority. It includes every turn that the
process was given an opportunity to run, whether or not it
took that opportunity. We try to keep the charges of all
processes about the same. *)
charge : real ref
(* ... *)
}
(* The argument that I give to dominators.
Some internal state is just global. *)
val reserved = Array.array (256, false)
fun gamestate (D { gs, ... }) = gs
fun getpid (D { pid, ... }) = pid
(* Indexed by pid. *)
val processes = GA.empty () : process GA.growarray
fun getpriority (D { pid, ... }) =
let val P { priority, ... } = GA.sub processes pid
in !priority
end
fun setpriority pid new_priority =
let val P { priority, ... } = GA.sub processes pid
in
priority := new_priority;
GA.appi (fn (child_pid, P { parent = SOME parent_pid, ... }) =>
if parent_pid = pid then setpriority child_pid new_priority else ()
| _ => ()) processes
end
fun getname (D { pid, ... }) =
let val P { name, ... } = GA.sub processes pid
in name
end
fun getslots (D { pid, ... }) =
let val P { reserved_slots, ... } = GA.sub processes pid
in !reserved_slots
end
fun longname pid =
let val P { parent, name, ... } = GA.sub processes pid
in case parent of
NONE => name
| SOME pid => longname pid ^ "." ^ name
end
(* PERF: Doesn't need to be linear time. *)
exception Return of int
fun reserve_addressable_slot (dos as D { pid, ... }) =
let
val P { reserved_slots, ... } = GA.sub processes pid
val (_, vitality) = GS.myside (gamestate dos)
in
Util.for 0 255
(fn i => let val slot = Array.sub (Numbers.addressability, i) in
if Array.sub (reserved, slot) orelse
(* Maybe should also prefer slots that have higher
health, if we don't care about addressability? *)
Array.sub (vitality, slot) <= 0
then ()
else (Array.update (reserved, slot, true);
reserved_slots := slot :: !reserved_slots;
raise Return slot) end);
NONE
end handle Return i => SOME i
(* We choose to skip the best 8 slots since they are easily
addressed (unless that's all that's left). *)
fun reserve_slot (dos as D { pid, ... }) =
let
val P { reserved_slots, ... } = GA.sub processes pid
val (prog, vitality) = GS.myside (gamestate dos)
(* only_empty requires us to use slots that don't have anything
in them. First try to reserve barren slots. *)
fun try only_empty i =
let val slot = Array.sub (Numbers.addressability, i) in
if Array.sub (reserved, slot) orelse
(* Maybe should also prefer slots that have higher
health, if we don't care about addressability? *)
Array.sub (vitality, slot) <= 0 orelse
(only_empty andalso
Array.sub (prog, slot) <> LTG.VFn LTG.VI)
then ()
else (Array.update (reserved, slot, true);
reserved_slots := slot :: !reserved_slots;
raise Return slot)
end
in
Util.for 8 255 (try true);
Util.for 0 7 (try true);
Util.for 8 255 (try false);
Util.for 0 7 (try false);
NONE
end handle Return i => SOME i
fun reserve_fixed_slot (D { pid, ... }) i =
let val P { reserved_slots, ... } = GA.sub processes pid
in
if Array.sub (reserved, i) then false
else (Array.update (reserved, i, true);
reserved_slots := i :: !reserved_slots;
true)
end
fun reserve_fixed_slots (D { pid, ... }) l =
let val P { reserved_slots, ... } = GA.sub processes pid
in
if List.exists (fn i => Array.sub (reserved, i)) l
then false
else (app (fn i => (Array.update (reserved, i, true);
reserved_slots := i :: !reserved_slots)) l;
true)
end
fun release_slot_by_pid pid i =
let val P { reserved_slots, ... } = GA.sub processes pid
val rest =
case ListUtil.extract (fn j => i = j) (!reserved_slots) of
SOME (_, rest) => rest
| NONE => raise DOS ("Releasing slot " ^ Int.toString i ^ " not owned by you")
in
if Array.sub (reserved, i)
then (Array.update (reserved, i, false);
reserved_slots := rest)
else raise DOS ("Released unreserved slot " ^ Int.toString i)
end
fun release_slot (D { pid, ... }) i = release_slot_by_pid pid i
fun release_all_slots (dos as D { pid, ... }) =
let val P { reserved_slots, ... } = GA.sub processes pid
in
app (release_slot dos) (!reserved_slots)
end
fun transfer_slot dos {dst, slot} =
let in
release_slot dos slot
handle DOS _ => raise DOS ("Transfered unreserved slot " ^ Int.toString slot);
(* This should always succeed *)
ignore (reserve_fixed_slot dst slot orelse raise DOS ("Failed transfer"))
end
fun is_reserved i = Array.sub (reserved, i)
(* How many game turns have passed. *)
val turnnum = ref 0
fun getturnnumber () = !turnnum
val rtos = Real.fmt (StringCvt.FIX (SOME 2))
fun spawn parent (name, priority, f) =
let
val total_charge = ref 0.0
val length = ref 0
val () = GA.app (fn (P { charge, ... }) =>
(total_charge := !total_charge + !charge;
length := !length + 1))
processes
val charge = if !length = 0 then 0.0
else !total_charge / (Real.fromInt (!length))
(*
val () = eprint ("[DOS] new process started with charge " ^
rtos (!charge) ^ "\n")
*)
val pid = GA.update_next processes (P { reserved_slots = ref nil,
name = name,
priority = ref priority,
parent = parent,
dominator = f,
charge = ref charge})
in
pid
end
(* A list of processes that have been killed this turn. They should not
have another chance to run. *)
val killed_this_turn = ref []
fun kill pid =
let
val P { parent, reserved_slots, ... } = GA.sub processes pid
in
killed_this_turn := pid :: !killed_this_turn;
GA.appi (fn (child_pid, P { parent = SOME parent_pid, ... }) =>
if parent_pid = pid then kill child_pid else ()
| _ => ()) processes;
(* XXX
We thought maybe parents should get their children's slots when
children are killed but then we changed our minds
(* If we have parent, move reserved_slots to it. Otherwise, free them. *)
case parent of
SOME parent_pid =>
let val P { reserved_slots = parent_reserved_slots, ... } =
GA.sub processes parent_pid
in
parent_reserved_slots := !reserved_slots @ !parent_reserved_slots
end
| NONE => app (release_slot_by_pid pid) (!reserved_slots);
*)
app (release_slot_by_pid pid) (!reserved_slots);
GA.erase processes pid
end
fun makelayer (doms : (string * real * dominator) list) =
let
(* Starting processes *)
val () = app (ignore o spawn NONE) doms
(* TODO: everybody in queue gets to see new state. *)
val current_pid = ref 0
fun dos_init _ = ()
fun dos_taketurn (gs : GS.gamestate) =
let
(* Every dominator gets a preview of its state, and
might adjust its priority. *)
val () = GA.appi (fn (pid, P { dominator, ... }) =>
let val dos = D { gs = gs, pid = pid }
in #preview dominator dos
end) processes
val l = ref nil
(* Compute the total charge that the process would have if we ran
it this term. The favors the higher priority process when two
processes have the same current charge. *)
val () = GA.appi (fn (pid, P {charge, priority, ... }) =>
l := (pid, !charge + (1.0 / !priority)) :: !l)
processes
val l = ListUtil.sort (ListUtil.bysecond Real.compare) (!l)
(*
val () =
eprint ("[DOS]: Schedulable: " ^ StringUtil.delimit " "
(map (fn (pid, charge) =>
(if List.exists (fn p => p = pid) (!killed_this_turn)
then "<killed>" else longname pid)
^ "@" ^ rtos charge) l) ^ "\n")
*)
val had_chance_to_run = ref []
val actual_charge = ref ~1.0
fun dosomething nil = LTG.LeftApply (LTG.I, 0) (* Idle! *)
| dosomething ((pid, new_charge) :: t) =
if List.exists (fn p => p = pid) (!killed_this_turn) then dosomething t else
let
val P { dominator, ... } = GA.sub processes pid
val dos = D { gs = gs, pid = pid }
in
had_chance_to_run := pid :: !had_chance_to_run;
case #taketurn dominator dos of
Can'tRun => dosomething t
| Turn turn =>
let in
(*
eprint ("[DOS] sched: " ^ Int.toString (!turnnum)
^ " " ^ (if List.exists (fn p => p = pid) (!killed_this_turn)
then "<killed>" else longname pid) ^ "\n");
*)
(* Save what was charged. *)
actual_charge := new_charge;
turn
end
end
in
dosomething l
before
((* Everyone gets charged, but they only get charged what the
process that ran did.*)
if !actual_charge >= 0.0 then
app (fn pid =>
if GA.has processes pid then (* It might have died *)
let val P { charge, ... } = GA.sub processes pid
in
(* eprint ("Setting charge of " ^ longname pid
^ " to " ^ rtos (!actual_charge) ^ "\n"); *)
charge := !actual_charge end
else ()) (!had_chance_to_run)
else ();
turnnum := !turnnum + 1;
killed_this_turn := [])
end
in
(dos_init, dos_taketurn)
end
end