mirror of https://github.com/m-labs/artiq.git
Implement recording of DMA traces on the core device.
This commit is contained in:
parent
3a1f14c16c
commit
5d3b00cf12
|
@ -0,0 +1,55 @@
|
||||||
|
from artiq.language.core import syscall, kernel
|
||||||
|
from artiq.language.types import TStr, TNone
|
||||||
|
|
||||||
|
from numpy import int64
|
||||||
|
|
||||||
|
|
||||||
|
@syscall
|
||||||
|
def dma_record_start() -> TNone:
|
||||||
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
|
|
||||||
|
@syscall
|
||||||
|
def dma_record_stop(name: TStr) -> TNone:
|
||||||
|
raise NotImplementedError("syscall not simulated")
|
||||||
|
|
||||||
|
|
||||||
|
class DMARecordContextManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ""
|
||||||
|
self.saved_now_mu = int64(0)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def __enter__(self):
|
||||||
|
"""Starts recording a DMA trace. All RTIO operations are redirected to
|
||||||
|
a newly created DMA buffer after this call, and ``now`` is reset to zero."""
|
||||||
|
dma_record_start() # this may raise, so do it before altering now
|
||||||
|
self.saved_now_mu = now_mu()
|
||||||
|
at_mu(0)
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
"""Stops recording a DMA trace. All recorded RTIO operations are stored
|
||||||
|
in a newly created trace called ``self.name``, and ``now`` is restored
|
||||||
|
to the value it had before ``__enter__`` was called."""
|
||||||
|
dma_record_stop(self.name) # see above
|
||||||
|
at_mu(self.saved_now_mu)
|
||||||
|
|
||||||
|
|
||||||
|
class CoreDMA:
|
||||||
|
"""Core device Direct Memory Access (DMA) driver.
|
||||||
|
|
||||||
|
Gives access to the DMA functionality of the core device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
kernel_invariants = {"core", "recorder"}
|
||||||
|
|
||||||
|
def __init__(self, dmgr, core_device="core"):
|
||||||
|
self.core = dmgr.get(core_device)
|
||||||
|
self.recorder = DMARecordContextManager()
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def record(self, name):
|
||||||
|
"""Returns a context manager that will record a DMA trace called ``name``."""
|
||||||
|
self.recorder.name = name
|
||||||
|
return self.recorder
|
|
@ -120,6 +120,10 @@ class RTIOOverflow(Exception):
|
||||||
"""
|
"""
|
||||||
artiq_builtin = True
|
artiq_builtin = True
|
||||||
|
|
||||||
|
class DMAError(Exception):
|
||||||
|
"""Raised when performing an invalid DMA operation."""
|
||||||
|
artiq_builtin = True
|
||||||
|
|
||||||
class DDSError(Exception):
|
class DDSError(Exception):
|
||||||
"""Raised when attempting to start a DDS batch while already in a batch,
|
"""Raised when attempting to start a DDS batch while already in a batch,
|
||||||
when too many commands are batched, and when DDS channel settings are
|
when too many commands are batched, and when DDS channel settings are
|
||||||
|
|
|
@ -20,6 +20,11 @@
|
||||||
"module": "artiq.coredevice.cache",
|
"module": "artiq.coredevice.cache",
|
||||||
"class": "CoreCache"
|
"class": "CoreCache"
|
||||||
},
|
},
|
||||||
|
"core_dma": {
|
||||||
|
"type": "local",
|
||||||
|
"module": "artiq.coredevice.dma",
|
||||||
|
"class": "CoreDMA"
|
||||||
|
},
|
||||||
"core_dds": {
|
"core_dds": {
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"module": "artiq.coredevice.dds",
|
"module": "artiq.coredevice.dds",
|
||||||
|
|
|
@ -105,6 +105,9 @@ static mut API: &'static [(&'static str, *const ())] = &[
|
||||||
api!(rtio_input_timestamp = ::rtio::input_timestamp),
|
api!(rtio_input_timestamp = ::rtio::input_timestamp),
|
||||||
api!(rtio_input_data = ::rtio::input_data),
|
api!(rtio_input_data = ::rtio::input_data),
|
||||||
|
|
||||||
|
api!(dma_record_start = ::dma_record_start),
|
||||||
|
api!(dma_record_stop = ::dma_record_stop),
|
||||||
|
|
||||||
api!(drtio_get_channel_state = ::rtio::drtio_dbg::get_channel_state),
|
api!(drtio_get_channel_state = ::rtio::drtio_dbg::get_channel_state),
|
||||||
api!(drtio_reset_channel_state = ::rtio::drtio_dbg::reset_channel_state),
|
api!(drtio_reset_channel_state = ::rtio::drtio_dbg::reset_channel_state),
|
||||||
api!(drtio_get_fifo_space = ::rtio::drtio_dbg::get_fifo_space),
|
api!(drtio_get_fifo_space = ::rtio::drtio_dbg::get_fifo_space),
|
||||||
|
|
|
@ -36,11 +36,11 @@ fn recv<R, F: FnOnce(&Message) -> R>(f: F) -> R {
|
||||||
|
|
||||||
macro_rules! recv {
|
macro_rules! recv {
|
||||||
($p:pat => $e:expr) => {
|
($p:pat => $e:expr) => {
|
||||||
recv(|request| {
|
recv(move |request| {
|
||||||
if let $p = request {
|
if let $p = request {
|
||||||
$e
|
$e
|
||||||
} else {
|
} else {
|
||||||
send(&Log(format_args!("unexpected reply: {:?}", request)));
|
send(&Log(format_args!("unexpected reply: {:?}\n", request)));
|
||||||
loop {}
|
loop {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -50,7 +50,7 @@ macro_rules! recv {
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[lang = "panic_fmt"]
|
#[lang = "panic_fmt"]
|
||||||
pub extern fn panic_fmt(args: core::fmt::Arguments, file: &'static str, line: u32) -> ! {
|
pub extern fn panic_fmt(args: core::fmt::Arguments, file: &'static str, line: u32) -> ! {
|
||||||
send(&Log(format_args!("panic at {}:{}: {}", file, line, args)));
|
send(&Log(format_args!("panic at {}:{}: {}\n", file, line, args)));
|
||||||
send(&RunAborted);
|
send(&RunAborted);
|
||||||
loop {}
|
loop {}
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,7 @@ mod api;
|
||||||
mod rtio;
|
mod rtio;
|
||||||
|
|
||||||
static mut NOW: u64 = 0;
|
static mut NOW: u64 = 0;
|
||||||
|
static mut LIBRARY: Option<Library<'static>> = None;
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern fn send_to_core_log(text: CSlice<u8>) {
|
pub extern fn send_to_core_log(text: CSlice<u8>) {
|
||||||
|
@ -236,6 +237,61 @@ extern fn i2c_read(busno: i32, ack: bool) -> i32 {
|
||||||
recv!(&I2cReadReply { data } => data) as i32
|
recv!(&I2cReadReply { data } => data) as i32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static mut DMA_RECORDING: bool = false;
|
||||||
|
|
||||||
|
extern fn dma_record_start() {
|
||||||
|
unsafe {
|
||||||
|
if DMA_RECORDING {
|
||||||
|
raise!("DMAError", "DMA is already recording")
|
||||||
|
}
|
||||||
|
|
||||||
|
let library = LIBRARY.as_ref().unwrap();
|
||||||
|
library.rebind(b"rtio_output",
|
||||||
|
dma_record_output as *const () as u32).unwrap();
|
||||||
|
library.rebind(b"rtio_output_wide",
|
||||||
|
dma_record_output_wide as *const () as u32).unwrap();
|
||||||
|
|
||||||
|
DMA_RECORDING = true;
|
||||||
|
send(&DmaRecordStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn dma_record_stop(name: CSlice<u8>) {
|
||||||
|
unsafe {
|
||||||
|
if !DMA_RECORDING {
|
||||||
|
raise!("DMAError", "DMA is not recording")
|
||||||
|
}
|
||||||
|
|
||||||
|
let library = LIBRARY.as_ref().unwrap();
|
||||||
|
library.rebind(b"rtio_output",
|
||||||
|
rtio::output as *const () as u32).unwrap();
|
||||||
|
library.rebind(b"rtio_output_wide",
|
||||||
|
rtio::output_wide as *const () as u32).unwrap();
|
||||||
|
|
||||||
|
DMA_RECORDING = false;
|
||||||
|
send(&DmaRecordStop(str::from_utf8(name.as_ref()).unwrap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn dma_record_output(timestamp: i64, channel: i32, address: i32, data: i32) {
|
||||||
|
send(&DmaRecordAppend {
|
||||||
|
timestamp: timestamp as u64,
|
||||||
|
channel: channel as u32,
|
||||||
|
address: address as u32,
|
||||||
|
data: &[data as u32]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn dma_record_output_wide(timestamp: i64, channel: i32, address: i32, data: CSlice<i32>) {
|
||||||
|
assert!(data.len() <= 16); // enforce the hardware limit
|
||||||
|
send(&DmaRecordAppend {
|
||||||
|
timestamp: timestamp as u64,
|
||||||
|
channel: channel as u32,
|
||||||
|
address: address as u32,
|
||||||
|
data: unsafe { mem::transmute::<&[i32], &[u32]>(data.as_ref()) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn attribute_writeback(typeinfo: *const ()) {
|
unsafe fn attribute_writeback(typeinfo: *const ()) {
|
||||||
struct Attr {
|
struct Attr {
|
||||||
offset: usize,
|
offset: usize,
|
||||||
|
@ -248,9 +304,6 @@ unsafe fn attribute_writeback(typeinfo: *const ()) {
|
||||||
objects: *const *const ()
|
objects: *const *const ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// artiq_compile'd kernels don't include type information
|
|
||||||
if typeinfo.is_null() { return }
|
|
||||||
|
|
||||||
let mut tys = typeinfo as *const *const Type;
|
let mut tys = typeinfo as *const *const Type;
|
||||||
while !(*tys).is_null() {
|
while !(*tys).is_null() {
|
||||||
let ty = *tys;
|
let ty = *tys;
|
||||||
|
@ -299,14 +352,21 @@ pub unsafe fn main() {
|
||||||
|
|
||||||
let __bss_start = library.lookup(b"__bss_start").unwrap();
|
let __bss_start = library.lookup(b"__bss_start").unwrap();
|
||||||
let _end = library.lookup(b"_end").unwrap();
|
let _end = library.lookup(b"_end").unwrap();
|
||||||
|
let __modinit__ = library.lookup(b"__modinit__").unwrap();
|
||||||
|
let typeinfo = library.lookup(b"typeinfo");
|
||||||
|
|
||||||
|
LIBRARY = Some(library);
|
||||||
|
|
||||||
ptr::write_bytes(__bss_start as *mut u8, 0, (_end - __bss_start) as usize);
|
ptr::write_bytes(__bss_start as *mut u8, 0, (_end - __bss_start) as usize);
|
||||||
|
|
||||||
send(&NowInitRequest);
|
send(&NowInitRequest);
|
||||||
recv!(&NowInitReply(now) => NOW = now);
|
recv!(&NowInitReply(now) => NOW = now);
|
||||||
(mem::transmute::<u32, fn()>(library.lookup(b"__modinit__").unwrap()))();
|
(mem::transmute::<u32, fn()>(__modinit__))();
|
||||||
send(&NowSave(NOW));
|
send(&NowSave(NOW));
|
||||||
|
|
||||||
attribute_writeback(library.lookup(b"typeinfo").unwrap_or(0) as *const ());
|
if let Some(typeinfo) = typeinfo {
|
||||||
|
attribute_writeback(typeinfo as *const ());
|
||||||
|
}
|
||||||
|
|
||||||
send(&RunFinished);
|
send(&RunFinished);
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,6 @@ pub struct Library<'a> {
|
||||||
image_sz: usize,
|
image_sz: usize,
|
||||||
strtab: &'a [u8],
|
strtab: &'a [u8],
|
||||||
symtab: &'a [Elf32_Sym],
|
symtab: &'a [Elf32_Sym],
|
||||||
rela: &'a [Elf32_Rela],
|
|
||||||
pltrel: &'a [Elf32_Rela],
|
pltrel: &'a [Elf32_Rela],
|
||||||
hash_bucket: &'a [Elf32_Word],
|
hash_bucket: &'a [Elf32_Word],
|
||||||
hash_chain: &'a [Elf32_Word],
|
hash_chain: &'a [Elf32_Word],
|
||||||
|
@ -132,8 +131,9 @@ impl<'a> Library<'a> {
|
||||||
Ok(unsafe { *ptr = value })
|
Ok(unsafe { *ptr = value })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebind(&self, name: &[u8], addr: Elf32_Word) -> Result<(), Error<'a>> {
|
// This is unsafe because it mutates global data (the PLT).
|
||||||
for rela in self.rela.iter().chain(self.pltrel.iter()) {
|
pub unsafe fn rebind(&self, name: &[u8], addr: Elf32_Word) -> Result<(), Error<'a>> {
|
||||||
|
for rela in self.pltrel.iter() {
|
||||||
match ELF32_R_TYPE(rela.r_info) {
|
match ELF32_R_TYPE(rela.r_info) {
|
||||||
R_OR1K_32 | R_OR1K_GLOB_DAT | R_OR1K_JMP_SLOT => {
|
R_OR1K_32 | R_OR1K_GLOB_DAT | R_OR1K_JMP_SLOT => {
|
||||||
let sym = self.symtab.get(ELF32_R_SYM(rela.r_info) as usize)
|
let sym = self.symtab.get(ELF32_R_SYM(rela.r_info) as usize)
|
||||||
|
@ -315,7 +315,6 @@ impl<'a> Library<'a> {
|
||||||
image_sz: image.len(),
|
image_sz: image.len(),
|
||||||
strtab: strtab,
|
strtab: strtab,
|
||||||
symtab: symtab,
|
symtab: symtab,
|
||||||
rela: rela,
|
|
||||||
pltrel: pltrel,
|
pltrel: pltrel,
|
||||||
hash_bucket: &hash[..nbucket],
|
hash_bucket: &hash[..nbucket],
|
||||||
hash_chain: &hash[nbucket..nbucket + nchain],
|
hash_chain: &hash[nbucket..nbucket + nchain],
|
||||||
|
@ -331,9 +330,8 @@ impl<'a> Library<'a> {
|
||||||
// we never write to the memory they refer to, so it's safe.
|
// we never write to the memory they refer to, so it's safe.
|
||||||
mem::drop(image);
|
mem::drop(image);
|
||||||
|
|
||||||
for r in rela.iter().chain(pltrel.iter()) {
|
for r in rela { library.resolve_rela(r, resolve)? }
|
||||||
library.resolve_rela(r, resolve)?
|
for r in pltrel { library.resolve_rela(r, resolve)? }
|
||||||
}
|
|
||||||
|
|
||||||
Ok(library)
|
Ok(library)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,15 @@ pub enum Message<'a> {
|
||||||
|
|
||||||
RtioInitRequest,
|
RtioInitRequest,
|
||||||
|
|
||||||
|
DmaRecordStart,
|
||||||
|
DmaRecordAppend {
|
||||||
|
timestamp: u64,
|
||||||
|
channel: u32,
|
||||||
|
address: u32,
|
||||||
|
data: &'a [u32]
|
||||||
|
},
|
||||||
|
DmaRecordStop(&'a str),
|
||||||
|
|
||||||
DrtioChannelStateRequest { channel: u32 },
|
DrtioChannelStateRequest { channel: u32 },
|
||||||
DrtioChannelStateReply { fifo_space: u16, last_timestamp: u64 },
|
DrtioChannelStateReply { fifo_space: u16, last_timestamp: u64 },
|
||||||
DrtioResetChannelStateRequest { channel: u32 },
|
DrtioResetChannelStateRequest { channel: u32 },
|
||||||
|
|
|
@ -41,6 +41,7 @@ mod rtio_mgt;
|
||||||
mod urc;
|
mod urc;
|
||||||
mod sched;
|
mod sched;
|
||||||
mod cache;
|
mod cache;
|
||||||
|
mod rtio_dma;
|
||||||
|
|
||||||
mod kernel;
|
mod kernel;
|
||||||
mod session;
|
mod session;
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
use std::vec::Vec;
|
||||||
|
use std::string::String;
|
||||||
|
use std::btree_map::BTreeMap;
|
||||||
|
use std::mem;
|
||||||
|
use proto::io;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Entry {
|
||||||
|
data: Vec<u8>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Manager {
|
||||||
|
entries: BTreeMap<String, Entry>,
|
||||||
|
recording: Vec<u8>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new() -> Manager {
|
||||||
|
Manager {
|
||||||
|
entries: BTreeMap::new(),
|
||||||
|
recording: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_start(&mut self) {
|
||||||
|
self.recording.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_append(&mut self, timestamp: u64, channel: u32,
|
||||||
|
address: u32, data: &[u32]) {
|
||||||
|
// See gateware/rtio/dma.py.
|
||||||
|
let length = /*length*/1 + /*channel*/3 + /*timestamp*/8 + /*address*/2 +
|
||||||
|
/*data*/data.len() * 4;
|
||||||
|
let writer = &mut self.recording;
|
||||||
|
io::write_u8(writer, length as u8).unwrap();
|
||||||
|
io::write_u8(writer, (channel >> 24) as u8).unwrap();
|
||||||
|
io::write_u8(writer, (channel >> 16) as u8).unwrap();
|
||||||
|
io::write_u8(writer, (channel >> 8) as u8).unwrap();
|
||||||
|
io::write_u64(writer, timestamp).unwrap();
|
||||||
|
io::write_u16(writer, address as u16).unwrap();
|
||||||
|
for &word in data {
|
||||||
|
io::write_u32(writer, word).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_stop(&mut self, name: &str) {
|
||||||
|
let mut recorded = Vec::new();
|
||||||
|
mem::swap(&mut self.recording, &mut recorded);
|
||||||
|
recorded.shrink_to_fit();
|
||||||
|
|
||||||
|
info!("recorded DMA data: {:?}", recorded);
|
||||||
|
|
||||||
|
self.entries.insert(String::from(name), Entry {
|
||||||
|
data: recorded
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ use std::error::Error;
|
||||||
use {config, rtio_mgt, mailbox, rpc_queue, kernel};
|
use {config, rtio_mgt, mailbox, rpc_queue, kernel};
|
||||||
use logger_artiq::BufferLogger;
|
use logger_artiq::BufferLogger;
|
||||||
use cache::Cache;
|
use cache::Cache;
|
||||||
|
use rtio_dma::Manager as DmaManager;
|
||||||
use urc::Urc;
|
use urc::Urc;
|
||||||
use sched::{ThreadHandle, Io};
|
use sched::{ThreadHandle, Io};
|
||||||
use sched::{TcpListener, TcpStream};
|
use sched::{TcpListener, TcpStream};
|
||||||
|
@ -34,6 +35,7 @@ fn io_error(msg: &str) -> io::Error {
|
||||||
struct Congress {
|
struct Congress {
|
||||||
now: u64,
|
now: u64,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
|
dma_manager: DmaManager,
|
||||||
finished_cleanly: Cell<bool>
|
finished_cleanly: Cell<bool>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +44,7 @@ impl Congress {
|
||||||
Congress {
|
Congress {
|
||||||
now: 0,
|
now: 0,
|
||||||
cache: Cache::new(),
|
cache: Cache::new(),
|
||||||
|
dma_manager: DmaManager::new(),
|
||||||
finished_cleanly: Cell::new(true)
|
finished_cleanly: Cell::new(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,8 +350,7 @@ fn process_host_message(io: &Io,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_kern_message(io: &Io,
|
fn process_kern_message(io: &Io, mut stream: Option<&mut TcpStream>,
|
||||||
mut stream: Option<&mut TcpStream>,
|
|
||||||
session: &mut Session) -> io::Result<bool> {
|
session: &mut Session) -> io::Result<bool> {
|
||||||
kern_recv_notrace(io, |request| {
|
kern_recv_notrace(io, |request| {
|
||||||
match (request, session.kernel_state) {
|
match (request, session.kernel_state) {
|
||||||
|
@ -394,6 +396,19 @@ fn process_kern_message(io: &Io,
|
||||||
kern_acknowledge()
|
kern_acknowledge()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&kern::DmaRecordStart => {
|
||||||
|
session.congress.dma_manager.record_start();
|
||||||
|
kern_acknowledge()
|
||||||
|
}
|
||||||
|
&kern::DmaRecordAppend { timestamp, channel, address, data } => {
|
||||||
|
session.congress.dma_manager.record_append(timestamp, channel, address, data);
|
||||||
|
kern_acknowledge()
|
||||||
|
}
|
||||||
|
&kern::DmaRecordStop(name) => {
|
||||||
|
session.congress.dma_manager.record_stop(name);
|
||||||
|
kern_acknowledge()
|
||||||
|
}
|
||||||
|
|
||||||
&kern::DrtioChannelStateRequest { channel } => {
|
&kern::DrtioChannelStateRequest { channel } => {
|
||||||
let (fifo_space, last_timestamp) = rtio_mgt::drtio_dbg::get_channel_state(channel);
|
let (fifo_space, last_timestamp) = rtio_mgt::drtio_dbg::get_channel_state(channel);
|
||||||
kern_send(io, &kern::DrtioChannelStateReply { fifo_space: fifo_space,
|
kern_send(io, &kern::DrtioChannelStateReply { fifo_space: fifo_space,
|
||||||
|
|
|
@ -21,6 +21,12 @@ These drivers are for the core device and the peripherals closely integrated int
|
||||||
.. automodule:: artiq.coredevice.dds
|
.. automodule:: artiq.coredevice.dds
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
:mod:`artiq.coredevice.dma` module
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: artiq.coredevice.dma
|
||||||
|
:members:
|
||||||
|
|
||||||
:mod:`artiq.coredevice.spi` module
|
:mod:`artiq.coredevice.spi` module
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue