1
Optimizing KVM's PIR harvesting using the same techniques as posted MSIs,
1
Optimizing KVM's PIR harvesting using the same techniques as posted MSIs,
2
most notably to use 8-byte accesses on 64-bit kernels (/facepalm).
2
most notably to use 8-byte accesses on 64-bit kernels (/facepalm).
3
3
4
Fix a few warts along the way, and finish up by adding a helper to dedup
4
Fix a few warts along the way, and finish up by adding a helper to dedup
5
the PIR harvesting code between KVM and posted MSIs.
5
the PIR harvesting code between KVM and posted MSIs.
6
7
v2:
8
- Collect a review. [tglx]
9
- Use an "unsigned long" with a bitwise-OR to gather PIR. [tglx]
10
11
v1: https://lore.kernel.org/all/20250315030630.2371712-1-seanjc@google.com
6
12
7
Sean Christopherson (8):
13
Sean Christopherson (8):
8
x86/irq: Ensure initial PIR loads are performed exactly once
14
x86/irq: Ensure initial PIR loads are performed exactly once
9
x86/irq: Track if IRQ was found in PIR during initial loop (to load
15
x86/irq: Track if IRQ was found in PIR during initial loop (to load
10
PIR vals)
16
PIR vals)
...
...
14
KVM: VMX: Isolate pure loads from atomic XCHG when processing PIR
20
KVM: VMX: Isolate pure loads from atomic XCHG when processing PIR
15
KVM: VMX: Use arch_xchg() when processing PIR to avoid instrumentation
21
KVM: VMX: Use arch_xchg() when processing PIR to avoid instrumentation
16
x86/irq: KVM: Add helper for harvesting PIR to deduplicate KVM and
22
x86/irq: KVM: Add helper for harvesting PIR to deduplicate KVM and
17
posted MSIs
23
posted MSIs
18
24
19
arch/x86/include/asm/posted_intr.h | 79 +++++++++++++++++++++++++++---
25
arch/x86/include/asm/posted_intr.h | 78 +++++++++++++++++++++++++++---
20
arch/x86/kernel/irq.c | 63 ++++--------------------
26
arch/x86/kernel/irq.c | 63 ++++--------------------
21
arch/x86/kvm/lapic.c | 20 ++++----
27
arch/x86/kvm/lapic.c | 20 ++++----
22
arch/x86/kvm/lapic.h | 4 +-
28
arch/x86/kvm/lapic.h | 4 +-
23
arch/x86/kvm/vmx/posted_intr.h | 2 +-
29
arch/x86/kvm/vmx/posted_intr.h | 2 +-
24
5 files changed, 96 insertions(+), 72 deletions(-)
30
5 files changed, 95 insertions(+), 72 deletions(-)
25
31
26
32
27
base-commit: c9ea48bb6ee6b28bbc956c1e8af98044618fed5e
33
base-commit: 782f9feaa9517caf33186dcdd6b50a8f770ed29b
28
--
34
--
29
2.49.0.rc1.451.g8f38331e32-goog
35
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
1
Ensure the PIR is read exactly once at the start of handle_pending_pir(),
1
Ensure the PIR is read exactly once at the start of handle_pending_pir(),
2
to guarantee that checking for an outstanding posted interrupt in a given
2
to guarantee that checking for an outstanding posted interrupt in a given
3
chuck doesn't reload the chunk from the "real" PIR. Functionally, a reload
3
chuck doesn't reload the chunk from the "real" PIR. Functionally, a reload
4
is benign, but it would defeat the purpose of pre-loading into a copy.
4
is benign, but it would defeat the purpose of pre-loading into a copy.
5
5
6
Fixes: 1b03d82ba15e ("x86/irq: Install posted MSI notification handler")
6
Fixes: 1b03d82ba15e ("x86/irq: Install posted MSI notification handler")
7
Cc: Jacob Pan <jacob.jun.pan@linux.intel.com>
7
Reviewed-by: Thomas Gleixner <tglx@linutronix.de>
8
Signed-off-by: Sean Christopherson <seanjc@google.com>
8
Signed-off-by: Sean Christopherson <seanjc@google.com>
9
---
9
---
10
arch/x86/kernel/irq.c | 2 +-
10
arch/x86/kernel/irq.c | 2 +-
11
1 file changed, 1 insertion(+), 1 deletion(-)
11
1 file changed, 1 insertion(+), 1 deletion(-)
12
12
...
...
22
+        pir_copy[i] = READ_ONCE(pir[i]);
22
+        pir_copy[i] = READ_ONCE(pir[i]);
23
23
24
    for (i = 0; i < 4; i++) {
24
    for (i = 0; i < 4; i++) {
25
        if (!pir_copy[i])
25
        if (!pir_copy[i])
26
--
26
--
27
2.49.0.rc1.451.g8f38331e32-goog
27
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
1
Track whether or not at least one IRQ was found in PIR during the initial
1
Track whether or not at least one IRQ was found in PIR during the initial
2
loop to load PIR chunks from memory. Doing so generates slightly better
2
loop to load PIR chunks from memory. Doing so generates slightly better
3
code (arguably), especially for the case where there are no pending IRQs.
3
code (arguably) for processing the for-loop of XCHGs, especially for the
4
case where there are no pending IRQs.
4
5
5
Note, while PIR can be modified between the initial load and the XCHG, it
6
Note, while PIR can be modified between the initial load and the XCHG, it
6
can only _gain_ new IRQs, i.e. there is no danger of a false positive due
7
can only _gain_ new IRQs, i.e. there is no danger of a false positive due
7
to the final version of pir_copy[] being empty.
8
to the final version of pir_copy[] being empty.
8
9
9
Opportunistically rename the boolean in anticipation of moving the PIR
10
Opportunistically convert the boolean to an "unsigned long" and compute
11
the effective boolean result via bitwise-OR. Some compilers, e.g.
12
clang-14, need the extra "hint" to elide conditional branches.
13
14
Opportunistically rename the variable in anticipation of moving the PIR
10
accesses to a common helper that can be shared by posted MSIs and KVM.
15
accesses to a common helper that can be shared by posted MSIs and KVM.
11
16
12
Old:
17
Old:
13
<+74>:    test %rdx,%rdx
18
<+74>:    test %rdx,%rdx
14
<+77>:    je 0xffffffff812bbeb0 <handle_pending_pir+144>
19
<+77>:    je 0xffffffff812bbeb0 <handle_pending_pir+144>
...
...
53
<+144>:    je 0xffffffff812bbebd <handle_pending_pir+157>
58
<+144>:    je 0xffffffff812bbebd <handle_pending_pir+157>
54
<pir[3]>
59
<pir[3]>
55
60
56
Signed-off-by: Sean Christopherson <seanjc@google.com>
61
Signed-off-by: Sean Christopherson <seanjc@google.com>
57
---
62
---
58
arch/x86/kernel/irq.c | 19 +++++++++++--------
63
arch/x86/kernel/irq.c | 19 ++++++++++---------
59
1 file changed, 11 insertions(+), 8 deletions(-)
64
1 file changed, 10 insertions(+), 9 deletions(-)
60
65
61
diff --git a/arch/x86/kernel/irq.c b/arch/x86/kernel/irq.c
66
diff --git a/arch/x86/kernel/irq.c b/arch/x86/kernel/irq.c
62
index XXXXXXX..XXXXXXX 100644
67
index XXXXXXX..XXXXXXX 100644
63
--- a/arch/x86/kernel/irq.c
68
--- a/arch/x86/kernel/irq.c
64
+++ b/arch/x86/kernel/irq.c
69
+++ b/arch/x86/kernel/irq.c
65
@@ -XXX,XX +XXX,XX @@ static __always_inline bool handle_pending_pir(u64 *pir, struct pt_regs *regs)
70
@@ -XXX,XX +XXX,XX @@ void intel_posted_msi_init(void)
71
*/
72
static __always_inline bool handle_pending_pir(u64 *pir, struct pt_regs *regs)
66
{
73
{
74
+    unsigned long pir_copy[4], pending = 0;
67
    int i, vec = FIRST_EXTERNAL_VECTOR;
75
    int i, vec = FIRST_EXTERNAL_VECTOR;
68
    unsigned long pir_copy[4];
76
-    unsigned long pir_copy[4];
69
-    bool handled = false;
77
-    bool handled = false;
70
+    bool found_irq = false;
71
78
72
-    for (i = 0; i < 4; i++)
79
-    for (i = 0; i < 4; i++)
73
+    for (i = 0; i < 4; i++) {
80
+    for (i = 0; i < 4; i++) {
74
        pir_copy[i] = READ_ONCE(pir[i]);
81
        pir_copy[i] = READ_ONCE(pir[i]);
75
+        if (pir_copy[i])
82
+        pending |= pir_copy[i];
76
+            found_irq = true;
77
+    }
83
+    }
78
+
84
+
79
+    if (!found_irq)
85
+    if (!pending)
80
+        return false;
86
+        return false;
81
87
82
    for (i = 0; i < 4; i++) {
88
    for (i = 0; i < 4; i++) {
83
        if (!pir_copy[i])
89
        if (!pir_copy[i])
84
            continue;
90
            continue;
...
...
98
+    return true;
104
+    return true;
99
}
105
}
100
106
101
/*
107
/*
102
--
108
--
103
2.49.0.rc1.451.g8f38331e32-goog
109
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
...
...
21
+        irr_val = READ_ONCE(*p_irr);
21
+        irr_val = READ_ONCE(*p_irr);
22
        pir_val = READ_ONCE(pir[i]);
22
        pir_val = READ_ONCE(pir[i]);
23
23
24
        if (pir_val) {
24
        if (pir_val) {
25
--
25
--
26
2.49.0.rc1.451.g8f38331e32-goog
26
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
...
...
78
*        read, xchg, read, xchg, read, xchg, read, xchg
78
*        read, xchg, read, xchg, read, xchg, read, xchg
79
*/
79
*/
80
-static __always_inline bool handle_pending_pir(u64 *pir, struct pt_regs *regs)
80
-static __always_inline bool handle_pending_pir(u64 *pir, struct pt_regs *regs)
81
+static __always_inline bool handle_pending_pir(unsigned long *pir, struct pt_regs *regs)
81
+static __always_inline bool handle_pending_pir(unsigned long *pir, struct pt_regs *regs)
82
{
82
{
83
-    unsigned long pir_copy[4], pending = 0;
84
+    unsigned long pir_copy[NR_PIR_WORDS], pending = 0;
83
    int i, vec = FIRST_EXTERNAL_VECTOR;
85
    int i, vec = FIRST_EXTERNAL_VECTOR;
84
-    unsigned long pir_copy[4];
85
+    unsigned long pir_copy[NR_PIR_WORDS];
86
    bool found_irq = false;
87
86
88
-    for (i = 0; i < 4; i++) {
87
-    for (i = 0; i < 4; i++) {
89
+    for (i = 0; i < NR_PIR_WORDS; i++) {
88
+    for (i = 0; i < NR_PIR_WORDS; i++) {
90
        pir_copy[i] = READ_ONCE(pir[i]);
89
        pir_copy[i] = READ_ONCE(pir[i]);
91
        if (pir_copy[i])
90
        pending |= pir_copy[i];
92
            found_irq = true;
91
    }
93
@@ -XXX,XX +XXX,XX @@ static __always_inline bool handle_pending_pir(u64 *pir, struct pt_regs *regs)
92
@@ -XXX,XX +XXX,XX @@ static __always_inline bool handle_pending_pir(u64 *pir, struct pt_regs *regs)
94
    if (!found_irq)
93
    if (!pending)
95
        return false;
94
        return false;
96
95
97
-    for (i = 0; i < 4; i++) {
96
-    for (i = 0; i < 4; i++) {
98
+    for (i = 0; i < NR_PIR_WORDS; i++) {
97
+    for (i = 0; i < NR_PIR_WORDS; i++) {
99
        if (!pir_copy[i])
98
        if (!pir_copy[i])
...
...
181
+    vec = find_last_bit(pi_desc->pir, 256);
180
+    vec = find_last_bit(pi_desc->pir, 256);
182
    return vec < 256 ? vec : -1;
181
    return vec < 256 ? vec : -1;
183
}
182
}
184
183
185
--
184
--
186
2.49.0.rc1.451.g8f38331e32-goog
185
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
...
...
55
+                irr_val = prev_irr_val | __pir[i];
55
+                irr_val = prev_irr_val | __pir[i];
56
            } while (prev_irr_val != irr_val &&
56
            } while (prev_irr_val != irr_val &&
57
                 !try_cmpxchg(p_irr, &prev_irr_val, irr_val));
57
                 !try_cmpxchg(p_irr, &prev_irr_val, irr_val));
58
58
59
--
59
--
60
2.49.0.rc1.451.g8f38331e32-goog
60
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
...
...
13
why isolating loads from XCHG is desirable.
13
why isolating loads from XCHG is desirable.
14
14
15
Suggested-by: Jim Mattson <jmattson@google.com>
15
Suggested-by: Jim Mattson <jmattson@google.com>
16
Signed-off-by: Sean Christopherson <seanjc@google.com>
16
Signed-off-by: Sean Christopherson <seanjc@google.com>
17
---
17
---
18
arch/x86/kvm/lapic.c | 9 +++++++++
18
arch/x86/kvm/lapic.c | 9 ++++++++-
19
1 file changed, 9 insertions(+)
19
1 file changed, 8 insertions(+), 1 deletion(-)
20
20
21
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
21
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
22
index XXXXXXX..XXXXXXX 100644
22
index XXXXXXX..XXXXXXX 100644
23
--- a/arch/x86/kvm/lapic.c
23
--- a/arch/x86/kvm/lapic.c
24
+++ b/arch/x86/kvm/lapic.c
24
+++ b/arch/x86/kvm/lapic.c
25
@@ -XXX,XX +XXX,XX @@ bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
25
@@ -XXX,XX +XXX,XX @@ static u8 count_vectors(void *bitmap)
26
27
bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
26
{
28
{
27
    unsigned long pir_vals[NR_PIR_WORDS];
29
-    unsigned long pir_vals[NR_PIR_WORDS];
30
+    unsigned long pir_vals[NR_PIR_WORDS], pending = 0;
28
    u32 *__pir = (void *)pir_vals;
31
    u32 *__pir = (void *)pir_vals;
29
+    bool found_irq = false;
30
    u32 i, vec;
32
    u32 i, vec;
31
    u32 irr_val, prev_irr_val;
33
    u32 irr_val, prev_irr_val;
32
    int max_updated_irr;
33
@@ -XXX,XX +XXX,XX @@ bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
34
@@ -XXX,XX +XXX,XX @@ bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
34
35
35
    for (i = 0; i < NR_PIR_WORDS; i++) {
36
    for (i = 0; i < NR_PIR_WORDS; i++) {
36
        pir_vals[i] = READ_ONCE(pir[i]);
37
        pir_vals[i] = READ_ONCE(pir[i]);
37
+        if (pir_vals[i])
38
+        pending |= pir_vals[i];
38
+            found_irq = true;
39
+    }
39
+    }
40
+
40
+
41
+    if (!found_irq)
41
+    if (!pending)
42
+        return false;
42
+        return false;
43
+
43
+
44
+    for (i = 0; i < NR_PIR_WORDS; i++) {
44
+    for (i = 0; i < NR_PIR_WORDS; i++) {
45
        if (!pir_vals[i])
45
        if (!pir_vals[i])
46
            continue;
46
            continue;
47
47
48
--
48
--
49
2.49.0.rc1.451.g8f38331e32-goog
49
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
...
...
20
+        pir_vals[i] = arch_xchg(&pir[i], 0);
20
+        pir_vals[i] = arch_xchg(&pir[i], 0);
21
    }
21
    }
22
22
23
    for (i = vec = 0; i <= 7; i++, vec += 32) {
23
    for (i = vec = 0; i <= 7; i++, vec += 32) {
24
--
24
--
25
2.49.0.rc1.451.g8f38331e32-goog
25
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib
...
...
3
3
4
No functional change intended.
4
No functional change intended.
5
5
6
Signed-off-by: Sean Christopherson <seanjc@google.com>
6
Signed-off-by: Sean Christopherson <seanjc@google.com>
7
---
7
---
8
arch/x86/include/asm/posted_intr.h | 65 ++++++++++++++++++++++++++++++
8
arch/x86/include/asm/posted_intr.h | 64 ++++++++++++++++++++++++++++++
9
arch/x86/kernel/irq.c | 50 +----------------------
9
arch/x86/kernel/irq.c | 50 ++---------------------
10
arch/x86/kvm/lapic.c | 16 +-------
10
arch/x86/kvm/lapic.c | 16 +-------
11
3 files changed, 68 insertions(+), 63 deletions(-)
11
3 files changed, 69 insertions(+), 61 deletions(-)
12
12
13
diff --git a/arch/x86/include/asm/posted_intr.h b/arch/x86/include/asm/posted_intr.h
13
diff --git a/arch/x86/include/asm/posted_intr.h b/arch/x86/include/asm/posted_intr.h
14
index XXXXXXX..XXXXXXX 100644
14
index XXXXXXX..XXXXXXX 100644
15
--- a/arch/x86/include/asm/posted_intr.h
15
--- a/arch/x86/include/asm/posted_intr.h
16
+++ b/arch/x86/include/asm/posted_intr.h
16
+++ b/arch/x86/include/asm/posted_intr.h
...
...
68
+ *        read, xchg, read, xchg, read, xchg, read, xchg
68
+ *        read, xchg, read, xchg, read, xchg, read, xchg
69
+ */
69
+ */
70
+static __always_inline bool pi_harvest_pir(unsigned long *pir,
70
+static __always_inline bool pi_harvest_pir(unsigned long *pir,
71
+                     unsigned long *pir_vals)
71
+                     unsigned long *pir_vals)
72
+{
72
+{
73
+    bool found_irq = false;
73
+    unsigned long pending = 0;
74
+    int i;
74
+    int i;
75
+
75
+
76
+    for (i = 0; i < NR_PIR_WORDS; i++) {
76
+    for (i = 0; i < NR_PIR_WORDS; i++) {
77
+        pir_vals[i] = READ_ONCE(pir[i]);
77
+        pir_vals[i] = READ_ONCE(pir[i]);
78
+        if (pir_vals[i])
78
+        pending |= pir_vals[i];
79
+            found_irq = true;
80
+    }
79
+    }
81
+
80
+
82
+    if (!found_irq)
81
+    if (!pending)
83
+        return false;
82
+        return false;
84
+
83
+
85
+    for (i = 0; i < NR_PIR_WORDS; i++) {
84
+    for (i = 0; i < NR_PIR_WORDS; i++) {
86
+        if (!pir_vals[i])
85
+        if (!pir_vals[i])
87
+            continue;
86
+            continue;
...
...
135
- * instead of:
134
- * instead of:
136
- *        read, xchg, read, xchg, read, xchg, read, xchg
135
- *        read, xchg, read, xchg, read, xchg, read, xchg
137
- */
136
- */
138
static __always_inline bool handle_pending_pir(unsigned long *pir, struct pt_regs *regs)
137
static __always_inline bool handle_pending_pir(unsigned long *pir, struct pt_regs *regs)
139
{
138
{
139
-    unsigned long pir_copy[NR_PIR_WORDS], pending = 0;
140
-    int i, vec = FIRST_EXTERNAL_VECTOR;
140
-    int i, vec = FIRST_EXTERNAL_VECTOR;
141
+    unsigned long pir_copy[NR_PIR_WORDS];
141
+    int vec = FIRST_EXTERNAL_VECTOR;
142
+    int vec = FIRST_EXTERNAL_VECTOR;
142
    unsigned long pir_copy[NR_PIR_WORDS];
143
-    bool found_irq = false;
144
143
145
-    for (i = 0; i < NR_PIR_WORDS; i++) {
144
-    for (i = 0; i < NR_PIR_WORDS; i++) {
146
-        pir_copy[i] = READ_ONCE(pir[i]);
145
-        pir_copy[i] = READ_ONCE(pir[i]);
147
-        if (pir_copy[i])
146
-        pending |= pir_copy[i];
148
-            found_irq = true;
147
-    }
149
-    }
148
-
150
-
149
-    if (!pending)
151
-    if (!found_irq)
152
+    if (!pi_harvest_pir(pir, pir_copy))
150
+    if (!pi_harvest_pir(pir, pir_copy))
153
        return false;
151
        return false;
154
152
155
-    for (i = 0; i < NR_PIR_WORDS; i++) {
153
-    for (i = 0; i < NR_PIR_WORDS; i++) {
156
-        if (!pir_copy[i])
154
-        if (!pir_copy[i])
...
...
164
162
165
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
163
diff --git a/arch/x86/kvm/lapic.c b/arch/x86/kvm/lapic.c
166
index XXXXXXX..XXXXXXX 100644
164
index XXXXXXX..XXXXXXX 100644
167
--- a/arch/x86/kvm/lapic.c
165
--- a/arch/x86/kvm/lapic.c
168
+++ b/arch/x86/kvm/lapic.c
166
+++ b/arch/x86/kvm/lapic.c
169
@@ -XXX,XX +XXX,XX @@ bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
167
@@ -XXX,XX +XXX,XX @@ static u8 count_vectors(void *bitmap)
168
169
bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
170
{
170
{
171
    unsigned long pir_vals[NR_PIR_WORDS];
171
-    unsigned long pir_vals[NR_PIR_WORDS], pending = 0;
172
+    unsigned long pir_vals[NR_PIR_WORDS];
172
    u32 *__pir = (void *)pir_vals;
173
    u32 *__pir = (void *)pir_vals;
173
-    bool found_irq = false;
174
    u32 i, vec;
174
    u32 i, vec;
175
    u32 irr_val, prev_irr_val;
175
    u32 irr_val, prev_irr_val;
176
    int max_updated_irr;
177
@@ -XXX,XX +XXX,XX @@ bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
176
@@ -XXX,XX +XXX,XX @@ bool __kvm_apic_update_irr(unsigned long *pir, void *regs, int *max_irr)
178
    max_updated_irr = -1;
177
    max_updated_irr = -1;
179
    *max_irr = -1;
178
    *max_irr = -1;
180
179
181
-    for (i = 0; i < NR_PIR_WORDS; i++) {
180
-    for (i = 0; i < NR_PIR_WORDS; i++) {
182
-        pir_vals[i] = READ_ONCE(pir[i]);
181
-        pir_vals[i] = READ_ONCE(pir[i]);
183
-        if (pir_vals[i])
182
-        pending |= pir_vals[i];
184
-            found_irq = true;
183
-    }
185
-    }
184
-
186
-
185
-    if (!pending)
187
-    if (!found_irq)
188
+    if (!pi_harvest_pir(pir, pir_vals))
186
+    if (!pi_harvest_pir(pir, pir_vals))
189
        return false;
187
        return false;
190
188
191
-    for (i = 0; i < NR_PIR_WORDS; i++) {
189
-    for (i = 0; i < NR_PIR_WORDS; i++) {
192
-        if (!pir_vals[i])
190
-        if (!pir_vals[i])
...
...
197
-
195
-
198
    for (i = vec = 0; i <= 7; i++, vec += 32) {
196
    for (i = vec = 0; i <= 7; i++, vec += 32) {
199
        u32 *p_irr = (u32 *)(regs + APIC_IRR + i * 0x10);
197
        u32 *p_irr = (u32 *)(regs + APIC_IRR + i * 0x10);
200
198
201
--
199
--
202
2.49.0.rc1.451.g8f38331e32-goog
200
2.49.0.472.ge94155a9ec-goog
diff view generated by jsdifflib