1
The following changes since commit d01beac177d44491d7db8747b79d94e1b53d173b:
1
The following changes since commit 3521ade3510eb5cefb2e27a101667f25dad89935:
2
2
3
Merge remote-tracking branch 'remotes/kraxel/tags/vga-20180507-pull-request' into staging (2018-05-08 14:23:02 +0100)
3
Merge remote-tracking branch 'remotes/thuth-gitlab/tags/pull-request-2021-07-29' into staging (2021-07-29 13:17:20 +0100)
4
4
5
are available in the git repository at:
5
are available in the Git repository at:
6
6
7
git://github.com/codyprime/qemu-kvm-jtc.git tags/block-pull-request
7
https://gitlab.com/stefanha/qemu.git tags/block-pull-request
8
8
9
for you to fetch changes up to a2cb9239b7610ffb00f9ced5cd7640d40b0e1ccf:
9
for you to fetch changes up to cc8eecd7f105a1dff5876adeb238a14696061a4a:
10
10
11
sheepdog: Fix sd_co_create_opts() memory leaks (2018-05-08 10:47:27 -0400)
11
MAINTAINERS: Added myself as a reviewer for the NVMe Block Driver (2021-07-29 17:17:34 +0100)
12
12
13
----------------------------------------------------------------
13
----------------------------------------------------------------
14
Block patches
14
Pull request
15
16
The main fix here is for io_uring. Spurious -EAGAIN errors can happen and the
17
request needs to be resubmitted.
18
19
The MAINTAINERS changes carry no risk and we might as well include them in QEMU
20
6.1.
21
15
----------------------------------------------------------------
22
----------------------------------------------------------------
16
23
17
Kevin Wolf (1):
24
Fabian Ebner (1):
18
sheepdog: Fix sd_co_create_opts() memory leaks
25
block/io_uring: resubmit when result is -EAGAIN
19
26
20
Max Reitz (2):
27
Philippe Mathieu-Daudé (1):
21
block/mirror: Make cancel always cancel pre-READY
28
MAINTAINERS: Added myself as a reviewer for the NVMe Block Driver
22
iotests: Add test for cancelling a mirror job
23
29
24
Stefan Hajnoczi (1):
30
Stefano Garzarella (1):
25
block/mirror: honor ratelimit again
31
MAINTAINERS: add Stefano Garzarella as io_uring reviewer
26
32
27
block/mirror.c | 14 +++--
33
MAINTAINERS | 2 ++
28
block/sheepdog.c | 4 +-
34
block/io_uring.c | 16 +++++++++++++++-
29
tests/qemu-iotests/185.out | 4 +-
35
2 files changed, 17 insertions(+), 1 deletion(-)
30
tests/qemu-iotests/218 | 138 +++++++++++++++++++++++++++++++++++++++++++++
31
tests/qemu-iotests/218.out | 30 ++++++++++
32
tests/qemu-iotests/group | 1 +
33
6 files changed, 183 insertions(+), 8 deletions(-)
34
create mode 100644 tests/qemu-iotests/218
35
create mode 100644 tests/qemu-iotests/218.out
36
36
37
--
37
--
38
2.13.6
38
2.31.1
39
39
40
diff view generated by jsdifflib
Deleted patch
1
From: Stefan Hajnoczi <stefanha@redhat.com>
2
1
3
Commit b76e4458b1eb3c32e9824fe6aa51f67d2b251748 ("block/mirror: change
4
the semantic of 'force' of block-job-cancel") accidentally removed the
5
ratelimit in the mirror job.
6
7
Reintroduce the ratelimit but keep the block-job-cancel force=true
8
behavior that was added in commit
9
b76e4458b1eb3c32e9824fe6aa51f67d2b251748.
10
11
Note that block_job_sleep_ns() returns immediately when the job is
12
cancelled. Therefore it's safe to unconditionally call
13
block_job_sleep_ns() - a cancelled job does not sleep.
14
15
This commit fixes the non-deterministic qemu-iotests 185 output. The
16
test relies on the ratelimit to make the job sleep until the 'quit'
17
command is processed. Previously the job could complete before the
18
'quit' command was received since there was no ratelimit.
19
20
Cc: Liang Li <liliang.opensource@gmail.com>
21
Cc: Jeff Cody <jcody@redhat.com>
22
Cc: Kevin Wolf <kwolf@redhat.com>
23
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
24
Message-id: 20180424123527.19168-1-stefanha@redhat.com
25
Signed-off-by: Jeff Cody <jcody@redhat.com>
26
---
27
block/mirror.c | 8 +++++---
28
tests/qemu-iotests/185.out | 4 ++--
29
2 files changed, 7 insertions(+), 5 deletions(-)
30
31
diff --git a/block/mirror.c b/block/mirror.c
32
index XXXXXXX..XXXXXXX 100644
33
--- a/block/mirror.c
34
+++ b/block/mirror.c
35
@@ -XXX,XX +XXX,XX @@ static void coroutine_fn mirror_run(void *opaque)
36
}
37
38
ret = 0;
39
+
40
+ if (s->synced && !should_complete) {
41
+ delay_ns = (s->in_flight == 0 && cnt == 0 ? SLICE_TIME : 0);
42
+ }
43
trace_mirror_before_sleep(s, cnt, s->synced, delay_ns);
44
+ block_job_sleep_ns(&s->common, delay_ns);
45
if (block_job_is_cancelled(&s->common) && s->common.force) {
46
break;
47
- } else if (!should_complete) {
48
- delay_ns = (s->in_flight == 0 && cnt == 0 ? SLICE_TIME : 0);
49
- block_job_sleep_ns(&s->common, delay_ns);
50
}
51
s->last_pause_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
52
}
53
diff --git a/tests/qemu-iotests/185.out b/tests/qemu-iotests/185.out
54
index XXXXXXX..XXXXXXX 100644
55
--- a/tests/qemu-iotests/185.out
56
+++ b/tests/qemu-iotests/185.out
57
@@ -XXX,XX +XXX,XX @@ Formatting 'TEST_DIR/t.qcow2', fmt=qcow2 size=67108864 backing_file=TEST_DIR/t.q
58
{"return": {}}
59
Formatting 'TEST_DIR/t.qcow2.copy', fmt=qcow2 size=67108864 cluster_size=65536 lazy_refcounts=off refcount_bits=16
60
{"return": {}}
61
+{"return": {}}
62
+{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "SHUTDOWN", "data": {"guest": false}}
63
{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_READY", "data": {"device": "disk", "len": 4194304, "offset": 4194304, "speed": 65536, "type": "mirror"}}
64
-{"return": {}}
65
-{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "SHUTDOWN", "data": {"guest": false}}
66
{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_JOB_COMPLETED", "data": {"device": "disk", "len": 4194304, "offset": 4194304, "speed": 65536, "type": "mirror"}}
67
68
=== Start backup job and exit qemu ===
69
--
70
2.13.6
71
72
diff view generated by jsdifflib
1
From: Kevin Wolf <kwolf@redhat.com>
1
From: Stefano Garzarella <sgarzare@redhat.com>
2
2
3
Both the option string for the 'redundancy' option and the
3
I've been working with io_uring for a while so I'd like to help
4
SheepdogRedundancy object that is created accordingly could be leaked in
4
with reviews.
5
error paths. This fixes the memory leaks.
6
5
7
Reported by Coverity (CID 1390614 and 1390641).
6
Signed-off-by: Stefano Garzarella <sgarzare@redhat.com>
7
Message-Id: <20210728131515.131045-1-sgarzare@redhat.com>
8
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
9
---
10
MAINTAINERS | 1 +
11
1 file changed, 1 insertion(+)
8
12
9
Signed-off-by: Kevin Wolf <kwolf@redhat.com>
13
diff --git a/MAINTAINERS b/MAINTAINERS
10
Message-id: 20180503153509.22223-1-kwolf@redhat.com
14
index XXXXXXX..XXXXXXX 100644
11
Reviewed-by: Jeff Cody <jcody@redhat.com>
15
--- a/MAINTAINERS
12
Signed-off-by: Jeff Cody <jcody@redhat.com>
16
+++ b/MAINTAINERS
13
---
17
@@ -XXX,XX +XXX,XX @@ Linux io_uring
14
block/sheepdog.c | 4 +++-
18
M: Aarushi Mehta <mehta.aaru20@gmail.com>
15
1 file changed, 3 insertions(+), 1 deletion(-)
19
M: Julia Suvorova <jusual@redhat.com>
20
M: Stefan Hajnoczi <stefanha@redhat.com>
21
+R: Stefano Garzarella <sgarzare@redhat.com>
22
L: qemu-block@nongnu.org
23
S: Maintained
24
F: block/io_uring.c
25
--
26
2.31.1
16
27
17
diff --git a/block/sheepdog.c b/block/sheepdog.c
18
index XXXXXXX..XXXXXXX 100644
19
--- a/block/sheepdog.c
20
+++ b/block/sheepdog.c
21
@@ -XXX,XX +XXX,XX @@ static SheepdogRedundancy *parse_redundancy_str(const char *opt)
22
} else {
23
ret = qemu_strtol(n2, NULL, 10, &parity);
24
if (ret < 0) {
25
+ g_free(redundancy);
26
return NULL;
27
}
28
29
@@ -XXX,XX +XXX,XX @@ static int coroutine_fn sd_co_create_opts(const char *filename, QemuOpts *opts,
30
QDict *qdict, *location_qdict;
31
QObject *crumpled;
32
Visitor *v;
33
- const char *redundancy;
34
+ char *redundancy;
35
Error *local_err = NULL;
36
int ret;
37
38
@@ -XXX,XX +XXX,XX @@ static int coroutine_fn sd_co_create_opts(const char *filename, QemuOpts *opts,
39
fail:
40
qapi_free_BlockdevCreateOptions(create_options);
41
qobject_unref(qdict);
42
+ g_free(redundancy);
43
return ret;
44
}
45
46
--
47
2.13.6
48
49
diff view generated by jsdifflib
1
From: Max Reitz <mreitz@redhat.com>
1
From: Fabian Ebner <f.ebner@proxmox.com>
2
2
3
We already have an extensive mirror test (041) which does cover
3
Linux SCSI can throw spurious -EAGAIN in some corner cases in its
4
cancelling a mirror job, especially after it has emitted the READY
4
completion path, which will end up being the result in the completed
5
event. However, it does not check what exact events are emitted after
5
io_uring request.
6
block-job-cancel is executed. More importantly, it does not use
7
throttling to ensure that it covers the case of block-job-cancel before
8
READY.
9
6
10
It would be possible to add this case to 041, but considering it is
7
Resubmitting such requests should allow block jobs to complete, even
11
already our largest test file, it makes sense to create a new file for
8
if such spurious errors are encountered.
12
these cases.
13
9
14
Signed-off-by: Max Reitz <mreitz@redhat.com>
10
Co-authored-by: Stefan Hajnoczi <stefanha@gmail.com>
15
Message-id: 20180501220509.14152-3-mreitz@redhat.com
11
Reviewed-by: Stefano Garzarella <sgarzare@redhat.com>
16
Signed-off-by: Jeff Cody <jcody@redhat.com>
12
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
13
Message-id: 20210729091029.65369-1-f.ebner@proxmox.com
14
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
17
---
15
---
18
tests/qemu-iotests/218 | 138 +++++++++++++++++++++++++++++++++++++++++++++
16
block/io_uring.c | 16 +++++++++++++++-
19
tests/qemu-iotests/218.out | 30 ++++++++++
17
1 file changed, 15 insertions(+), 1 deletion(-)
20
tests/qemu-iotests/group | 1 +
21
3 files changed, 169 insertions(+)
22
create mode 100644 tests/qemu-iotests/218
23
create mode 100644 tests/qemu-iotests/218.out
24
18
25
diff --git a/tests/qemu-iotests/218 b/tests/qemu-iotests/218
19
diff --git a/block/io_uring.c b/block/io_uring.c
26
new file mode 100644
27
index XXXXXXX..XXXXXXX
28
--- /dev/null
29
+++ b/tests/qemu-iotests/218
30
@@ -XXX,XX +XXX,XX @@
31
+#!/usr/bin/env python
32
+#
33
+# This test covers what happens when a mirror block job is cancelled
34
+# in various phases of its existence.
35
+#
36
+# Note that this test only checks the emitted events (i.e.
37
+# BLOCK_JOB_COMPLETED vs. BLOCK_JOB_CANCELLED), it does not compare
38
+# whether the target is in sync with the source when the
39
+# BLOCK_JOB_COMPLETED event occurs. This is covered by other tests
40
+# (such as 041).
41
+#
42
+# Copyright (C) 2018 Red Hat, Inc.
43
+#
44
+# This program is free software; you can redistribute it and/or modify
45
+# it under the terms of the GNU General Public License as published by
46
+# the Free Software Foundation; either version 2 of the License, or
47
+# (at your option) any later version.
48
+#
49
+# This program is distributed in the hope that it will be useful,
50
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
51
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
52
+# GNU General Public License for more details.
53
+#
54
+# You should have received a copy of the GNU General Public License
55
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
56
+#
57
+# Creator/Owner: Max Reitz <mreitz@redhat.com>
58
+
59
+import iotests
60
+from iotests import log
61
+
62
+iotests.verify_platform(['linux'])
63
+
64
+
65
+# Launches the VM, adds two null-co nodes (source and target), and
66
+# starts a blockdev-mirror job on them.
67
+#
68
+# Either both or none of speed and buf_size must be given.
69
+
70
+def start_mirror(vm, speed=None, buf_size=None):
71
+ vm.launch()
72
+
73
+ ret = vm.qmp('blockdev-add',
74
+ node_name='source',
75
+ driver='null-co',
76
+ size=1048576)
77
+ assert ret['return'] == {}
78
+
79
+ ret = vm.qmp('blockdev-add',
80
+ node_name='target',
81
+ driver='null-co',
82
+ size=1048576)
83
+ assert ret['return'] == {}
84
+
85
+ if speed is not None:
86
+ ret = vm.qmp('blockdev-mirror',
87
+ job_id='mirror',
88
+ device='source',
89
+ target='target',
90
+ sync='full',
91
+ speed=speed,
92
+ buf_size=buf_size)
93
+ else:
94
+ ret = vm.qmp('blockdev-mirror',
95
+ job_id='mirror',
96
+ device='source',
97
+ target='target',
98
+ sync='full')
99
+
100
+ assert ret['return'] == {}
101
+
102
+
103
+log('')
104
+log('=== Cancel mirror job before convergence ===')
105
+log('')
106
+
107
+log('--- force=false ---')
108
+log('')
109
+
110
+with iotests.VM() as vm:
111
+ # Low speed so it does not converge
112
+ start_mirror(vm, 65536, 65536)
113
+
114
+ log('Cancelling job')
115
+ log(vm.qmp('block-job-cancel', device='mirror', force=False))
116
+
117
+ log(vm.event_wait('BLOCK_JOB_CANCELLED'),
118
+ filters=[iotests.filter_qmp_event])
119
+
120
+log('')
121
+log('--- force=true ---')
122
+log('')
123
+
124
+with iotests.VM() as vm:
125
+ # Low speed so it does not converge
126
+ start_mirror(vm, 65536, 65536)
127
+
128
+ log('Cancelling job')
129
+ log(vm.qmp('block-job-cancel', device='mirror', force=True))
130
+
131
+ log(vm.event_wait('BLOCK_JOB_CANCELLED'),
132
+ filters=[iotests.filter_qmp_event])
133
+
134
+
135
+log('')
136
+log('=== Cancel mirror job after convergence ===')
137
+log('')
138
+
139
+log('--- force=false ---')
140
+log('')
141
+
142
+with iotests.VM() as vm:
143
+ start_mirror(vm)
144
+
145
+ log(vm.event_wait('BLOCK_JOB_READY'),
146
+ filters=[iotests.filter_qmp_event])
147
+
148
+ log('Cancelling job')
149
+ log(vm.qmp('block-job-cancel', device='mirror', force=False))
150
+
151
+ log(vm.event_wait('BLOCK_JOB_COMPLETED'),
152
+ filters=[iotests.filter_qmp_event])
153
+
154
+log('')
155
+log('--- force=true ---')
156
+log('')
157
+
158
+with iotests.VM() as vm:
159
+ start_mirror(vm)
160
+
161
+ log(vm.event_wait('BLOCK_JOB_READY'),
162
+ filters=[iotests.filter_qmp_event])
163
+
164
+ log('Cancelling job')
165
+ log(vm.qmp('block-job-cancel', device='mirror', force=True))
166
+
167
+ log(vm.event_wait('BLOCK_JOB_CANCELLED'),
168
+ filters=[iotests.filter_qmp_event])
169
diff --git a/tests/qemu-iotests/218.out b/tests/qemu-iotests/218.out
170
new file mode 100644
171
index XXXXXXX..XXXXXXX
172
--- /dev/null
173
+++ b/tests/qemu-iotests/218.out
174
@@ -XXX,XX +XXX,XX @@
175
+
176
+=== Cancel mirror job before convergence ===
177
+
178
+--- force=false ---
179
+
180
+Cancelling job
181
+{u'return': {}}
182
+{u'timestamp': {u'seconds': 'SECS', u'microseconds': 'USECS'}, u'data': {u'device': u'mirror', u'type': u'mirror', u'speed': 65536, u'len': 1048576, u'offset': 65536}, u'event': u'BLOCK_JOB_CANCELLED'}
183
+
184
+--- force=true ---
185
+
186
+Cancelling job
187
+{u'return': {}}
188
+{u'timestamp': {u'seconds': 'SECS', u'microseconds': 'USECS'}, u'data': {u'device': u'mirror', u'type': u'mirror', u'speed': 65536, u'len': 1048576, u'offset': 65536}, u'event': u'BLOCK_JOB_CANCELLED'}
189
+
190
+=== Cancel mirror job after convergence ===
191
+
192
+--- force=false ---
193
+
194
+{u'timestamp': {u'seconds': 'SECS', u'microseconds': 'USECS'}, u'data': {u'device': u'mirror', u'type': u'mirror', u'speed': 0, u'len': 1048576, u'offset': 1048576}, u'event': u'BLOCK_JOB_READY'}
195
+Cancelling job
196
+{u'return': {}}
197
+{u'timestamp': {u'seconds': 'SECS', u'microseconds': 'USECS'}, u'data': {u'device': u'mirror', u'type': u'mirror', u'speed': 0, u'len': 1048576, u'offset': 1048576}, u'event': u'BLOCK_JOB_COMPLETED'}
198
+
199
+--- force=true ---
200
+
201
+{u'timestamp': {u'seconds': 'SECS', u'microseconds': 'USECS'}, u'data': {u'device': u'mirror', u'type': u'mirror', u'speed': 0, u'len': 1048576, u'offset': 1048576}, u'event': u'BLOCK_JOB_READY'}
202
+Cancelling job
203
+{u'return': {}}
204
+{u'timestamp': {u'seconds': 'SECS', u'microseconds': 'USECS'}, u'data': {u'device': u'mirror', u'type': u'mirror', u'speed': 0, u'len': 1048576, u'offset': 1048576}, u'event': u'BLOCK_JOB_CANCELLED'}
205
diff --git a/tests/qemu-iotests/group b/tests/qemu-iotests/group
206
index XXXXXXX..XXXXXXX 100644
20
index XXXXXXX..XXXXXXX 100644
207
--- a/tests/qemu-iotests/group
21
--- a/block/io_uring.c
208
+++ b/tests/qemu-iotests/group
22
+++ b/block/io_uring.c
209
@@ -XXX,XX +XXX,XX @@
23
@@ -XXX,XX +XXX,XX @@ static void luring_process_completions(LuringState *s)
210
211 rw auto quick
24
total_bytes = ret + luringcb->total_read;
211
212 rw auto quick
25
212
213 rw auto quick
26
if (ret < 0) {
213
+218 rw auto quick
27
- if (ret == -EINTR) {
28
+ /*
29
+ * Only writev/readv/fsync requests on regular files or host block
30
+ * devices are submitted. Therefore -EAGAIN is not expected but it's
31
+ * known to happen sometimes with Linux SCSI. Submit again and hope
32
+ * the request completes successfully.
33
+ *
34
+ * For more information, see:
35
+ * https://lore.kernel.org/io-uring/20210727165811.284510-3-axboe@kernel.dk/T/#u
36
+ *
37
+ * If the code is changed to submit other types of requests in the
38
+ * future, then this workaround may need to be extended to deal with
39
+ * genuine -EAGAIN results that should not be resubmitted
40
+ * immediately.
41
+ */
42
+ if (ret == -EINTR || ret == -EAGAIN) {
43
luring_resubmit(s, luringcb);
44
continue;
45
}
214
--
46
--
215
2.13.6
47
2.31.1
216
48
217
diff view generated by jsdifflib
1
From: Max Reitz <mreitz@redhat.com>
1
From: Philippe Mathieu-Daudé <philmd@redhat.com>
2
2
3
Commit b76e4458b1eb3c32e9824fe6aa51f67d2b251748 made the mirror block
3
I'm interested in following the activity around the NVMe bdrv.
4
job respect block-job-cancel's @force flag: With that flag set, it would
5
now always really cancel, even post-READY.
6
4
7
Unfortunately, it had a side effect: Without that flag set, it would now
5
Signed-off-by: Philippe Mathieu-Daudé <philmd@redhat.com>
8
never cancel, not even before READY. Considering that is an
6
Message-id: 20210728183340.2018313-1-philmd@redhat.com
9
incompatible change and not noted anywhere in the commit or the
7
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
10
description of block-job-cancel's @force parameter, this seems
8
---
11
unintentional and we should revert to the previous behavior, which is to
9
MAINTAINERS | 1 +
12
immediately cancel the job when block-job-cancel is called before source
10
1 file changed, 1 insertion(+)
13
and target are in sync (i.e. before the READY event).
14
11
15
Cc: qemu-stable@nongnu.org
12
diff --git a/MAINTAINERS b/MAINTAINERS
16
Buglink: https://bugzilla.redhat.com/show_bug.cgi?id=1572856
13
index XXXXXXX..XXXXXXX 100644
17
Reported-by: Yanan Fu <yfu@redhat.com>
14
--- a/MAINTAINERS
18
Signed-off-by: Max Reitz <mreitz@redhat.com>
15
+++ b/MAINTAINERS
19
Reviewed-by: Eric Blake <eblake@redhat.com>
16
@@ -XXX,XX +XXX,XX @@ F: block/null.c
20
Message-id: 20180501220509.14152-2-mreitz@redhat.com
17
NVMe Block Driver
21
Reviewed-by: Jeff Cody <jcody@redhat.com>
18
M: Stefan Hajnoczi <stefanha@redhat.com>
22
Signed-off-by: Jeff Cody <jcody@redhat.com>
19
R: Fam Zheng <fam@euphon.net>
23
---
20
+R: Philippe Mathieu-Daudé <philmd@redhat.com>
24
block/mirror.c | 4 +++-
21
L: qemu-block@nongnu.org
25
1 file changed, 3 insertions(+), 1 deletion(-)
22
S: Supported
23
F: block/nvme*
24
--
25
2.31.1
26
26
27
diff --git a/block/mirror.c b/block/mirror.c
28
index XXXXXXX..XXXXXXX 100644
29
--- a/block/mirror.c
30
+++ b/block/mirror.c
31
@@ -XXX,XX +XXX,XX @@ static void coroutine_fn mirror_run(void *opaque)
32
}
33
trace_mirror_before_sleep(s, cnt, s->synced, delay_ns);
34
block_job_sleep_ns(&s->common, delay_ns);
35
- if (block_job_is_cancelled(&s->common) && s->common.force) {
36
+ if (block_job_is_cancelled(&s->common) &&
37
+ (!s->synced || s->common.force))
38
+ {
39
break;
40
}
41
s->last_pause_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
42
--
43
2.13.6
44
45
diff view generated by jsdifflib