Skip to content

Commit 7165ddf

Browse files
authored
Merge pull request #106 from KeithBauerANZ/reduce-stored-properties
Reduce code bloat from `FlagContainer`s
2 parents dc9ac0a + b097da9 commit 7165ddf

File tree

3 files changed

+198
-54
lines changed

3 files changed

+198
-54
lines changed

Sources/Vexil/Decorator.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,6 @@ internal protocol Decorated {
1515
func decorate (lookup: Lookup, label: String, codingPath: [String], config: VexilConfiguration)
1616
}
1717

18-
/// An internal class that `Flag` and `FlagGroup`s store their information in.
19-
/// It is specifically a class so that the `Flag` and `FlagGroup` structs can
20-
/// mutate the `Decorator` while remaining immutable themselves.
21-
///
22-
internal class Decorator {
23-
var key: String?
24-
weak var lookup: Lookup?
25-
26-
init() {}
27-
}
28-
2918
internal extension Sequence where Element == Mirror.Child {
3019

3120
typealias DecoratedChild = (label: String, value: Decorated)

Sources/Vexil/Flag.swift

Lines changed: 112 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,51 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
3030

3131
// MARK: - Properties
3232

33+
// FlagContainers may have many flags, so to reduce code bloat
34+
// it's important that each Flag have as few stored properties
35+
// (with nontrivial copy behavior) as possible. We therefore use
36+
// a single `Allocation` for all of Flag's stored properties.
37+
var allocation: Allocation
38+
3339
/// All `Flag`s are `Identifiable`
34-
public var id = UUID()
40+
public var id: UUID {
41+
get {
42+
allocation.id
43+
}
44+
set {
45+
if isKnownUniquelyReferenced(&allocation) == false {
46+
allocation = allocation.copy()
47+
}
48+
allocation.id = newValue
49+
}
50+
}
3551

3652
/// A collection of information about this `Flag`, such as its display name and description.
37-
public var info: FlagInfo
53+
public var info: FlagInfo {
54+
get {
55+
allocation.info
56+
}
57+
set {
58+
if isKnownUniquelyReferenced(&allocation) == false {
59+
allocation = allocation.copy()
60+
}
61+
allocation.info = newValue
62+
}
63+
}
3864

3965
/// The default value for this `Flag` for when no sources are available, or if no
4066
/// sources have a value specified for this flag.
41-
public var defaultValue: Value
67+
public var defaultValue: Value {
68+
get {
69+
allocation.defaultValue
70+
}
71+
set {
72+
if isKnownUniquelyReferenced(&allocation) == false {
73+
allocation = allocation.copy()
74+
}
75+
allocation.defaultValue = newValue
76+
}
77+
}
4278

4379
/// The `Flag` value. This is a calculated property based on the `FlagPole`s sources.
4480
public var wrappedValue: Value {
@@ -48,7 +84,7 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
4884
/// The string-based Key for this `Flag`, as calculated during `init`. This key is
4985
/// sent to the `FlagValueSource`s.
5086
public var key: String {
51-
return self.decorator.key!
87+
return self.allocation.key!
5288
}
5389

5490
/// A reference to the `Flag` itself is available as a projected value, in case you need
@@ -77,12 +113,12 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
77113
/// You can also specify `.hidden` to hide this flag from Vexillographer.
78114
///
79115
public init (name: String? = nil, codingKeyStrategy: CodingKeyStrategy = .default, default initialValue: Value, description: FlagInfo) {
80-
self.codingKeyStrategy = codingKeyStrategy
81-
self.defaultValue = initialValue
82-
83-
var info = description
84-
info.name = name
85-
self.info = info
116+
self.init(
117+
wrappedValue: initialValue,
118+
name: name,
119+
codingKeyStrategy: codingKeyStrategy,
120+
description: description
121+
)
86122
}
87123

88124
/// Initialises a new `Flag` with the supplied info.
@@ -101,46 +137,49 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
101137
/// You can also specify `.hidden` to hide this flag from Vexillographer.
102138
///
103139
public init (wrappedValue: Value, name: String? = nil, codingKeyStrategy: CodingKeyStrategy = .default, description: FlagInfo) {
104-
self.codingKeyStrategy = codingKeyStrategy
105-
self.defaultValue = wrappedValue
106-
107140
var info = description
108141
info.name = name
109-
self.info = info
142+
self.allocation = Allocation(
143+
info: info,
144+
defaultValue: wrappedValue,
145+
codingKeyStrategy: codingKeyStrategy
146+
)
110147
}
111148

112149

113150
// MARK: - Decorated Conformance
114151

115-
internal var decorator = Decorator()
116-
internal let codingKeyStrategy: CodingKeyStrategy
117-
118152
/// Decorates the receiver with the given lookup info.
119153
///
120154
/// `self.key` is calculated during this step based on the supplied parameters. `lookup` is used by `self.wrappedValue`
121155
/// to find out the current flag value from the source hierarchy.
122156
///
123-
internal func decorate (lookup: Lookup, label: String, codingPath: [String], config: VexilConfiguration) {
124-
self.decorator.lookup = lookup
125-
126-
var action = self.codingKeyStrategy.codingKey(label: label)
157+
internal func decorate (
158+
lookup: Lookup,
159+
label: String,
160+
codingPath: [String],
161+
config: VexilConfiguration
162+
) {
163+
self.allocation.lookup = lookup
164+
165+
var action = self.allocation.codingKeyStrategy.codingKey(label: label)
127166
if action == .default {
128167
action = config.codingPathStrategy.codingKey(label: label)
129168
}
130169

131170
switch action {
132171

133172
case .append(let string):
134-
self.decorator.key = (codingPath + [string])
173+
self.allocation.key = (codingPath + [string])
135174
.joined(separator: config.separator)
136175

137176
case .absolute(let string):
138-
self.decorator.key = string
177+
self.allocation.key = string
139178

140179
// these two options should really never happen, but just in case, use what we've got
141180
case .default, .skip:
142181
assertionFailure("Invalid `CodingKeyAction` found when attempting to create key name for Flag \(self)")
143-
self.decorator.key = (codingPath + [label])
182+
self.allocation.key = (codingPath + [label])
144183
.joined(separator: config.separator)
145184

146185
}
@@ -150,7 +189,7 @@ public struct Flag<Value>: Decorated, Identifiable where Value: FlagValue {
150189
// MARK: - Lookup Support
151190

152191
func value (in source: FlagValueSource?) -> LookupResult<Value>? {
153-
guard let lookup = self.decorator.lookup, let key = self.decorator.key else {
192+
guard let lookup = self.allocation.lookup, let key = self.allocation.key else {
154193
return LookupResult(source: nil, value: self.defaultValue)
155194
}
156195
let value: LookupResult<Value>? = lookup.lookup(key: key, in: source)
@@ -192,6 +231,52 @@ extension Flag: CustomDebugStringConvertible {
192231
}
193232

194233

234+
// MARK: - Property Storage
235+
236+
extension Flag {
237+
238+
final class Allocation {
239+
var id: UUID
240+
var info: FlagInfo
241+
var defaultValue: Value
242+
243+
// these are computed lazily during `decorate`
244+
var key: String?
245+
weak var lookup: Lookup?
246+
247+
var codingKeyStrategy: CodingKeyStrategy
248+
249+
init(
250+
id: UUID = UUID(),
251+
info: FlagInfo,
252+
defaultValue: Value,
253+
key: String? = nil,
254+
lookup: Lookup? = nil,
255+
codingKeyStrategy: CodingKeyStrategy
256+
) {
257+
self.id = id
258+
self.info = info
259+
self.defaultValue = defaultValue
260+
self.key = key
261+
self.lookup = lookup
262+
self.codingKeyStrategy = codingKeyStrategy
263+
}
264+
265+
func copy() -> Allocation {
266+
Allocation(
267+
id: id,
268+
info: info,
269+
defaultValue: defaultValue,
270+
key: key,
271+
lookup: lookup,
272+
codingKeyStrategy: codingKeyStrategy
273+
)
274+
}
275+
}
276+
277+
}
278+
279+
195280
// MARK: - Real Time Flag Publishing
196281

197282
#if !os(Linux)
@@ -206,7 +291,7 @@ public extension Flag where Value: FlagValue & Equatable {
206291
/// remove duplicates.
207292
///
208293
var publisher: AnyPublisher<Value, Never> {
209-
decorator.lookup!.publisher(key: self.key)
294+
allocation.lookup!.publisher(key: self.key)
210295
.removeDuplicates()
211296
.eraseToAnyPublisher()
212297
}
@@ -224,7 +309,7 @@ public extension Flag {
224309
/// remove duplicates.
225310
///
226311
var publisher: AnyPublisher<Value, Never> {
227-
decorator.lookup!.publisher(key: self.key)
312+
allocation.lookup!.publisher(key: self.key)
228313
}
229314

230315
}

Sources/Vexil/Group.swift

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,39 @@ import Foundation
1717
@propertyWrapper
1818
public struct FlagGroup<Group>: Decorated, Identifiable where Group: FlagContainer {
1919

20+
// FlagContainers may have many flag groups, so to reduce code bloat
21+
// it's important that each FlagGroup have as few stored properties
22+
// (with nontrivial copy behavior) as possible. We therefore use
23+
// a single `Allocation` for all of FlagGroup's stored properties.
24+
var allocation: Allocation
25+
2026
/// All `FlagGroup`s are `Identifiable`
21-
public let id = UUID()
27+
public var id: UUID {
28+
allocation.id
29+
}
2230

2331
/// A collection of information about this `FlagGroup` such as its display name and description.
24-
public let info: FlagInfo
32+
public var info: FlagInfo {
33+
allocation.info
34+
}
2535

2636
/// The `FlagContainer` being wrapped.
27-
public var wrappedValue: Group
37+
public var wrappedValue: Group {
38+
get {
39+
allocation.wrappedValue
40+
}
41+
set {
42+
if isKnownUniquelyReferenced(&allocation) == false {
43+
allocation = allocation.copy()
44+
}
45+
allocation.wrappedValue = newValue
46+
}
47+
}
2848

2949
/// How we should display this group in Vexillographer
30-
public let display: Display
50+
public var display: Display {
51+
allocation.display
52+
}
3153

3254

3355
// MARK: - Initialisation
@@ -48,28 +70,26 @@ public struct FlagGroup<Group>: Decorated, Identifiable where Group: FlagContain
4870
/// - display: Whether we should display this FlagGroup as using a `NavigationLink` or as a `Section` in Vexillographer
4971
///
5072
public init (name: String? = nil, codingKeyStrategy: CodingKeyStrategy = .default, description: FlagInfo, display: Display = .navigation) {
51-
self.codingKeyStrategy = codingKeyStrategy
52-
self.wrappedValue = Group()
53-
self.display = display
54-
5573
var info = description
5674
info.name = name
57-
self.info = info
75+
self.allocation = Allocation(
76+
info: info,
77+
wrappedValue: Group(),
78+
display: display,
79+
codingKeyStrategy: codingKeyStrategy
80+
)
5881
}
5982

6083

61-
// MARK: - Decoratod Conformance
62-
63-
internal var decorator = Decorator()
64-
private let codingKeyStrategy: CodingKeyStrategy
84+
// MARK: - Decorated Conformance
6585

6686
/// Decorates the receiver with the given lookup info.
6787
///
6888
/// The `key` for this part of the flag tree is calculated during this step based on the supplied parameters. All info is passed through to
6989
/// any `Flag` or `FlagGroup` contained within the receiver.
7090
///
7191
func decorate(lookup: Lookup, label: String, codingPath: [String], config: VexilConfiguration) {
72-
var action = self.codingKeyStrategy.codingKey(label: label)
92+
var action = self.allocation.codingKeyStrategy.codingKey(label: label)
7393
if action == .default {
7494
action = config.codingPathStrategy.codingKey(label: label)
7595
}
@@ -89,8 +109,9 @@ public struct FlagGroup<Group>: Decorated, Identifiable where Group: FlagContain
89109

90110
}
91111

92-
self.decorator.key = codingPath.joined(separator: config.separator)
93-
self.decorator.lookup = lookup
112+
// FIXME: for compatibility with existing behavior, this doesn't use `isKnownUniquelyReferenced`, but perhaps it should?
113+
self.allocation.key = codingPath.joined(separator: config.separator)
114+
self.allocation.lookup = lookup
94115

95116
Mirror(reflecting: self.wrappedValue)
96117
.children
@@ -135,6 +156,55 @@ extension FlagGroup: CustomDebugStringConvertible {
135156
}
136157

137158

159+
// MARK: - Property Storage
160+
161+
extension FlagGroup {
162+
163+
final class Allocation {
164+
let id: UUID
165+
let info: FlagInfo
166+
var wrappedValue: Group
167+
let display: Display
168+
169+
// these are computed lazily during `decorate`
170+
var key: String?
171+
weak var lookup: Lookup?
172+
173+
let codingKeyStrategy: CodingKeyStrategy
174+
175+
init(
176+
id: UUID = UUID(),
177+
info: FlagInfo,
178+
wrappedValue: Group,
179+
display: Display,
180+
key: String? = nil,
181+
lookup: Lookup? = nil,
182+
codingKeyStrategy: CodingKeyStrategy
183+
) {
184+
self.id = id
185+
self.info = info
186+
self.wrappedValue = wrappedValue
187+
self.display = display
188+
self.key = key
189+
self.lookup = lookup
190+
self.codingKeyStrategy = codingKeyStrategy
191+
}
192+
193+
func copy() -> Allocation {
194+
Allocation(
195+
info: info,
196+
wrappedValue: wrappedValue,
197+
display: display,
198+
key: key,
199+
lookup: lookup,
200+
codingKeyStrategy: codingKeyStrategy
201+
)
202+
}
203+
}
204+
205+
}
206+
207+
138208
// MARK: - Group Display
139209

140210
public extension FlagGroup {

0 commit comments

Comments
 (0)